안녕하세요 인텔리전스 엔지니어링팀 현건수입니다.
입사 후 제가 맡은 첫 프로젝트인 아이들나라 스튜디오를 소개하고, 운영 과정에서 겪었던 문제점과 개선 방안을 이야기해 보려고 합니다.
1. 아이들나라 스튜디오란?
현재 아이들나라는 키즈 컨텐츠 기반의 서비스를 운영 중입니다.
아이들나라 스튜디오는 컨텐츠에 AI 서비스를 접목하여 사용자에게 더 새로운 경험을 제공하기 위해 만들어진 admin 용 작업실입니다. 현재 시각 가장 대표적인 아웃풋은 리딩북입니다.
리딩북은 아이가 읽는 속도로 스스로 책장을 넘기며 읽을 수 있습니다. 성우가 읽어주고 목소리가 나오는 영역을 표시해 주는 포커스 기능이 있어 혼자서도 읽기 감각을 키우고 집중할 수 있습니다.
또한, 아이들이 책을 읽으면서 모르는 단어를 길게 터치하게 되면 아이들 눈높이로 단어를 풀이한 사전이 표출되고, 성우가 풀이를 읽어줍니다.
1-1. Workflow
동화책에 AI를 접목한 Workflow는 다음과 같습니다.
•
pre-process : 동화책 PDF를 리딩북 서비스에 맞게 이미지화하는 전처리 작업을 수행합니다.
•
OCR : 책 이미지에서 텍스트와 포커싱을 위한 좌표를 추출합니다.
•
TTS : OCR에서 추출한 텍스트로 음성 파일을 제작합니다.
•
STT : TTS로 제작한 음성 파일로 각각의 단어가 몇 초에 발음하는지 발음 시간을 추출합니다.
•
post-process : 단어 사전을 만들고, 위 결과들을 합치는 작업 등 후처리 작업을 수행합니다.
아이들나라 내에 AI를 다루는 팀이 없었기 때문에 외부 플랫폼에 의존하게 됐고, 각 Task에 적합한 플랫폼을 Repeat Test를 통해 선정했습니다. 한 가지 예를 들어, 한국어책인 경우 AWS, GCP 같은 글로벌 기업보다는 국내 기업들의 아웃풋이 저희의 니즈를 더 충족시켰고, 이와 반대로 영어책인 경우 글로벌 기업들의 아웃풋이 더 효과적인 것을 확인할 수 있었습니다.
2. 개선점 인식
아이들나라 스튜디오 Workflow 중 STT 과정에서 제작 시간이 가장 많이 소요됩니다. 왜냐하면 Audio 파일에서 각각의 단어 발음 시간이 올바르지 않게 오는 경우가 많기 때문에 이 부분을 운영팀에서 수동으로 수정하게 됩니다.
아래 이미지는 실제 아이들나라 스튜디오 STT의 결과물을 비주얼 라이징한 이미지입니다. Audio 파일에서 텍스트와 발음 시간을 추출했습니다.
•
원본 : 좀 타고 다녀볼까? 준수는 마지못해 할아버지를 따라나섰어
•
기댓값 : [“좀“, “타고”, “다녀볼까”, “준수는”, “마지못해”, “할아버지를”, “따라나섰어”]
•
실제값 : [“좀”, “타고”, “다녀볼까 준수는 마지못해 할아버지를 따라나섰어”]
기댓값과는 다르게 "다녀볼까", "준수는", "마지못해", "할아버지를", "따라나섰어" 의 각각의 문장들이 "다녀볼까 준수는 마지못해 할아버지를 따라나섰어" 라는 하나의 문자열로 추출되는 것을 확인할 수 있습니다. 이처럼 5개의 문장이 하나의 문장으로 추출되면, 4개의 문장을 더 생성해야 하며 각각의 발음 시간을 전부 확인하여 조정해야 하기 때문에 더 많은 운영 리소스를 사용하게 됩니다.
3. HuggingFace 모델 도입
STT의 문제점을 어떻게 해결할 수 있을까 고민하다가 아래 세 가지를 만족하는 모델이면 좋겠다고 결론을 내렸습니다.
•
성능 (필수)
•
커스텀마이징 (필수)
•
비용 문제 (선택)
1. STT의 문제점을 근본적으로 해결하기 위해 다른 무엇보다 성능이 현재 모델보다 좋아야 합니다. 성능이라고 하면 각 문장의 발음 시간을 정확히 추출하며 문장 단위로 정확히 끊어주는 것입니다.
2. 서비스를 운영하면서 비즈니스는 항상 변동할 수 있으며, 우리는 유연하게 대처할 수 있도록 대비해야 합니다. 하지만 지금은 외부 플랫폼에 의존하기 때문에 유연성이 많이 떨어진 상태입니다.
3. 외부 플랫폼을 사용한다는 것 자체만으로도 비용 문제는 항상 고려해야 하는 부분이었습니다. 하지만 기존 모델보다 성능적으로 뛰어나고 커스텀마이징이 된다면 비용적인 문제가 크게 차이 나지 않는다면 어느 정도 감안할 수 있을 것 같습니다.
외부 플랫폼은 커스텀마이징 할 수가 없어서 자체 모델을 내재화하는 방향으로 찾아보는 도중에 HuggingFace를 발견했습니다. HuggingFace는 머신러닝 진영에서 큰 커뮤니티를 이루고 있으며, 머신러닝 프레임워크를 제공하는 플랫폼입니다.
제가 느낀 큰 장점은 일반 사용자도 직접 모델 / 데이터셋을 공유할 수 있기 때문에 한 모델을 다양한 버전(언어, 모델 사이즈, 사전 모델 학습 시간 등)으로 배포할 수 있다는 것입니다. 또한 다양한 분류로 모델을 나눴기 때문에 원하는 모델을 쉽게 찾을 수 있었습니다.
3-1. Wav2Vec2
STT의 문제점을 개선하기 위해서, 음성파일을 텍스트로 변환하고 해당 텍스트가 몇 초에 발음하는 지를 추출하는 모델이 필요했습니다. 서치하는 도중, 2020년에 Facebook에서 자동 음성 인식(ASR)을 위한 모델인 Wav2Vec2를 찾았습니다. 기존에 자동 음성 인식(ASR)과 같은 자연어 처리(NLP) 작업을 수행할 때, Transformer 기반의 모델들을 사용했습니다. 기존 Transformer 기반의 모델은 성능은 좋으나 학습 시간과 비용이 매우 많이 소요된다는 단점이 있습니다.
Wav2Vec2은 Transformer의 모델을 라벨링 되지 않은 데이터로 53,000시간의 사전 학습을 진행했고, 10분간의 적은 양의 라벨링된 데이터로 fine-tuning을 진행해서 우수한 성능을 보였습니다.
Wav2Vec2 모델을 도식화해서 간단하게 플로우를 설명하면 아래와 같습니다
1.
Raw Audio : 음성 파일을 입력받습니다.
2.
CNN Encoder : 입력받은 음성 파일의 원시 sequence 신호를 매 시점마다 특정 길이의 값으로 인코딩해서 데이터의 특성을 추출합니다.
3.
Transformer : 문장 간의 연관관계를 이해하고 문맥을 파악합니다.
4.
CTC Algorithm: CTC 알고리즘을 사용해서 텍스트를 추출합니다.
3-2. 적정 모델 탐색
HuggingFace에서 모델을 찾을 때, 아래와 같이 4가지 요구사항을 충족시키는 모델을 찾길 원했습니다.
•
wav2vec2 기반 모델
•
한국어 지원
•
대중적인 모델
•
비교적 좋은 성능
모델을 찾던 중, wav2vec2 기반 모델을 한국어로 학습시킨 wav2vec2-large-xlsr-korean 이라는 모델을 발견했습니다. 타 모델들보다 조금은 대중적인 모델이 성능까지 뒷받침된다고 생각했기 때문에 지난달 다운로드 횟수를 확인해 보니 10만회를 넘었습니다. 성능은 WER: 4.74%(자동 음성 인식(ASR) 결과, 100개 단어 중 95개 일치) / CER: 1.78% (100개의 문자 중 98개 일치) 이기 때문에 저희 서비스에서 사용하기 준수한 모델이라고 판단했습니다.
3-3. SageMaker를 이용한 모델 적용
1.
Pull Model
•
HuggingFace에서 사전 학습된 wav2vec2-large-xlsr-korean Model과 Processor 로드
•
Processor : FeatureExtractor와 Tokenizer를 래핑한 객체로, FeatureExtractor와 Tokenizer의 기능을 사용하는 객체
2.
Upload Model
•
Load한 모델을 아카이브 파일(tar.gz ) 로 변환시켜서 S3에 업로드하고 모델 Image 생성
3.
Create an SageMaker Model
•
생성한 모델 Image를 기반한 Sagemaker Model 생성
4.
Create an SageMaker Endpoint Configuration
•
엔드포인트에 사용할 Configuration 생성
•
엔드포인트의 이름, 인스턴스 타입, 개수, 가중치 등 다양한 설정 가능
5.
Create an SageMaker Endpoint
•
4번에서 생성한 엔드포인트 Configuration으로 엔드포인트 배포
•
HuggingFaceModel의 deploy()는 model, endpoint configuration, endpoint를 모두 배포할 수 있도록 추상화돼 있기 때문에 쉽게 배포 가능
6.
Invoke Endpoint
•
배포된 엔드포인트 URL로 추론 API 호출
3-4. 모델 추론
•
요청
aws sagemaker-runtime invoke-endpoint \
--endpoint-name audio-test-endpoint \
--content-type audio/x-audio \
--body fileb://input-audio.mp3 \
output.txt
Shell
복사
•
결과
output.txt
=> 윤리는 집으로 걸어가면서 생각했어요 이곳은 나무들과 동료들이 행복하게 살던 아름다운 스피하 니다
Plain Text
복사
위에서 배포한 audio-test-endpoint 엔드포인트에 input-audio.mp3 파일을 추론 요청했더니 아웃풋으로 윤리는 집으로 걸어가면서 생각했어요 이곳은 나무들과 동료들이 행복하게 살던 아름다운 스피하 니다 라는 텍스트가 추출됐습니다. 텍스트가 정확히 일치하지는 않았지만, 저희는 OCR에서 추출한 텍스트를 서비스에 노출할 것이기 때문에 텍스트의 정확도 보다는 해당 텍스트가 몇 초에 발음되는지가 중요했습니다. 하지만 결과를 보면 텍스트만 추출되는 것을 확인할 수 있었고, 시간을 추출해야 하는 무언가가 필요했습니다.
3-5. 단어 offset 추출
HuggingFace Docs를 확인해 보면, 모델 아카이브 안에 code/inference.py 을 생성하면 추론할 때 HuggingFaceHandlerService 의 몇몇 메서드를 override해서 사용할 수 있다는 것을 확인했습니다. 여러 메서드들 중 추론할 때 호출되는 메서드인 predict_fn() 을 override하기로 했습니다.
predict_fn()을 아래와 같은 플로우로 재정의했습니다.
1.
모델로 입력된 오디오 데이터 로드 및 샘플률 변경
2.
오디오 데이터를 모델이 처리할 수 있는 형식으로 데이터 전처리
3.
각 음성에 맞는 토큰이 얼마나 일치하는지 점수 계산
4.
예측된 토큰을 다시 텍스트로 디코딩 (processor.batch_decode 파라미터에 output_word_offsets=True 설정)
5.
각 단어별 offset 추출
tokenizer의 batch_decode() 메서드에서 output_word_offsets 파라미터를 True로 설정하면 각 단어의 발음 시간을 추출할 수 있습니다. 위 코드에서는 processor의 batch_decode를 사용하고 있지만, processor는 음성에서 feature를 추출하는 feature_extractor와 token 관련 기능들을 수행하는 tokenizer를 래핑한 클래스이기 때문에 feature_extractor 와 tokenizer의 기능을 사용할 수 있습니다.
개선 결과 (화면)
•
before
•
after
기존 before 이미지와 HuggingFace 모델을 사용한 after 이미지를 비교해 보면 개선된 모습이 한 눈에 확인할 수 있습니다. 기존에 추출한 "다녀볼까 준수는 마지못해 할아버지를 따라나섰어" 라는 한 문장은 "다혀볼까", "진슨은", "마지", "못해", "할아버지를", "따라나셨어요" 라는 6문장으로 나눠서 추출됐습니다. 개선 후에 긴 문장을 잘게 나누는 작업이 줄었고, 해당 음성이 발음하는 곳을 더 정확히 판단하는 것 같다고 작업하기 더 수월하다는 피드백을 받았습니다.
4. 배포 후 운영
4-1. 비용 절감
기존 STT를 제작하기 위해선 외부 플랫폼을 이용했고, 현재는 HuggingFace 모델을 이용하기 때문에 두 방식의 비용 산정은 상이합니다.
•
기존 외부 플랫폼 : 초(s)당 과금 (1회 요청 기준, 음성 파일 재생 시간)
•
HuggingFace 모델 : 시간(h)당 과금 (인스턴스 사용 시간)
기존 외부 플랫폼을 이용할 경우, 1회 요청 기준 음성 파일의 재생 시간으로 과금했기 때문에 작업 시간에만 비용이 발생했습니다. 하지만 지금은 HuggingFace 모델을 직접 인스턴스에 올리는 방식이기 때문에, AWS에서 산정한 시간당 비용으로 과금해야합니다.
기존에는 작업시간만 비용이 발생했다면, 변경 후에는 퇴근 시간 이후이나 주말에도 계속 과금을 하는 방식입니다. 하지만 앞서 말씀드렸듯이, 아이들나라 스튜디오는 사용자에게 제공하는 어플리케이션이 아닌, admin 성 어플리케이션이기 때문에 작업 시간이 고정돼있는 현 상황에서 충분히 비용을 절감할 수 있겠다고 판단했습니다.
SageMaker 자체적으로 엔드포인트를 스케줄링 할 수 있는 기능이 따로 없어서, AWS Docs를 보니 SageMaker 엔드포인트에 대한 생성/삭제하는 API를 제공하고 있어서 SRE팀 도움을 받아 해당 기능을 사용하기로 했습니다.
1.
SageMaker Endpoint Configuration 설정
2.
EventBridge Scheduler를 사용해서 특정 시간에만 람다 호출
3.
Lambda에서는 SageMaker Endpoint 생성 및 삭제
첫 번째, 미리 SageMaker Endpoint의 Configuration을 생성했습니다. 왜냐하면 람다에서 생성할 엔드포인트가 어떤 모델을 배포할 지, 어떤 타입의 인스턴스를 사용할지,어떤 이름을 사용할지 등 미리 설정해야하기 때문입니다.
두 번째, 현재 아이들나라 스튜디오의 작업 시간은 09시부터 18시가 기준이기 때문에, EventBridge를 이용하여 상용 환경은 08시30분에는 생성 트리거, 18시30분에는 삭제 트리거를 발생시킵니다. (개발 환경은 09시와 18시로 설정)
세 번째, 람다에서는 미리 만들어둔 SageMaker Endpoint의 Configuration을 이용하여 엔드포인트를 생성/삭제하게 됩니다. 엔드포인트를 동일한 이름(ex: stt-model)으로 생성하기 때문에 엔드포인트를 바라보는 어플리케이션은 이를 신경쓰지 않아도 됩니다.
4-2. Mono, Stereo
아이들나라 스튜디오의 리딩북을 제작하기 위해서는 아래 두 가지 방식이 존재합니다.
•
TTS를 이용하여 목소리를 생성하는 방식
•
성우 파일을 업로드하여 자르는 방식
TTS를 이용해서 목소리를 생성하는 방식은 이슈가 없었지만, 성우 파일을 업로드해서 자르는 방식을 이용하면 모델의 입력 데이터와 레이어의 기대 입력 차원 간의 불일치로 에러가 발생했습니다.
"message": "Expected 2D (unbatched) or 3D (batched) input to conv1d, but got input of size: [1, 1, 863471, 1]"
Plain Text
복사
원인을 찾아보다 정상적으로 응답을 받는 음원과 에러가 발생하는 음원의 차이점을 확인해 보니, 음원의 채널이 달랐습니다. 음원의 채널은 오디오 신호에서 동시에 전송되는 독립된 신호 스트림의 수를 나타냅니다.
•
Mono : 모든 오디오가 하나의 신호로 구성되어 왼쪽(L)과 오른쪽(R) 스피커에 동일하게 출력합니다.
•
Stereo : 두 개의 채널을 가지며, 왼쪽(L)과 오른쪽(R) 스피커에 각각 다른 신호를 전달합니다. 이는 오디오에 공간적인 효과와 깊이를 부여하여 듣는 사람에게 좀 더 현실적인 청취 경험을 제공합니다.
•
2.1채널 / 5.1 채널 / 7.1 채널 : 두 개의 채널보다 더 많아지는 채널로, 더 뛰어난 현장감과 공간감을 제공합니다.
저희 모델은 Mono에서 정상 동작했으며, Mono보다 다차원인 Stereo가 인풋으로 들어왔기 때문에 에러가 발생했습니다. 그럼 이제 원인은 파악했고, 음원의 채널을 바꾸는 작업을 해야하는데 이 작업을 시점에 하면 좋을까? 고민이 시작됐습니다.
1.
SageMaker 모델에 요청하기 전에 작업
2.
TTS나 성우 파일 업로드할 때, 변환해서 저장
3.
SageMaker 모델 안에서 자체적으로 변경
세 가지의 방법이 떠올랐으며, 3번 방법이 가장 서비스에 알맞겠다고 생각했습니다.
왜냐하면 1번 방법은, 채널을 변환한 음성 파일이 하나 더 생성되는 것이기 때문에 똑같은 문장에 대한 음성 파일이 2개가 되어 삭제하거나, 리소스 용량, 위치 등 추가적인 관리 포인트가 늘어나게 됩니다. 만약, 저장을 따로 하지 않고 서비스 어플리케이션 내에서 변환 후에 해당 스트림만 SageMaker 모델에 요청할 수 있다고 해도, 이 변환 작업을 서비스 어플리케이션 내에서 하는 것이 과연 올바른 걸까?, 또 다른 어플리케이션에서 요청할 수 있을 텐데 그 어플리케이션에서도 변환 작업을 하는 것은 바람직하지 않다고 판단했습니다.
2번 방법은, origin한 source 음성 파일을 가지고 있을 수 없다는 단점이 있으며, 항상 채널이 변경된 오디오를 가지고 있어야 합니다. 채널을 변환한 음성 파일이 항상 정상적인 응답을 해준다고 보장할 수 없기 때문에 origin한 음성 파일을 분석해 보고 확인해야 하는 시점이 올 수 있다고 생각했습니다.
3번 방법은, 모델에 요청하는 어플리케이션은 추가적인 코드 디펜던시 없이 어떤 음성 파일이나 호출할 수 있으며, 추가적인 공간이나 관리 포인트가 필요하지 않습니다. 채널을 변환하는 작업으로 메모리 공간을 조금 더 차지할 수 있지만, 일시적으로 늘어나고 바로 소멸하기 때문에 3번 방법이 가장 알맞다고 판단했습니다.
3번의 방법으로 찾아보다가 librosa 라는 라이브러리를 발견했습니다. librosa는 음성 파일을 로드할 때, 음성 파일을 아주 쉽게 옵션만으로 채널을 Mono로 변환할 수 있었습니다. librosa.load() 메서드는 기본적으로 mono = True로 디폴트 값으로 제공하지만, 팀원들이 알아볼 수 있도록 명시적으로 작성했습니다.
마치며
이번 작업을 진행하면서 많은 것을 느꼈습니다.
우선 아이들나라 스튜디오 오픈 후에 개선점을 인식하고, 이를 개선하기 위해 저희한테 가장 맞는 솔루션을 찾는 과정부터 새로운 문제를 해결하는 고민의 시작이라고 생각했기 때문에 재밌게 시작할 수 있었습니다.
그리고 올해 팀원들이랑 스터디하면서 처음으로 머신러닝을 접했습니다. ‘모두 처음 접하는 분야인데 잘할 수 있을까?’ 라는 생각이 들었지만 스터디를 할수록 ‘이거 해볼 수도 있겠는데?’ 라는 생각으로 바뀌게 됐습니다. 이번에 도입한 모델은 사전 학습 모델이지만, 스터디하면서 실제로 상용에 도입했기 때문에 뿌듯했고, ‘하면 되는구나’ 라는 성취감을 많이 느꼈습니다. 또한 조직원이 조금 더 수월하게 작업할 수 있도록 도움 준 것 같아 보람찬 마음도 들었던 것 같습니다.
하지만 다소 아쉬웠던 점은, 위에 작성한 것 외에도 몇몇 운영상 이슈가 더 있었지만 이번 글에서는 다루지 못했습니다. 발생한 이슈들은 해결했지만, 아직 머신러닝에 대한 이해도가 깊지 않아서 발생한 이유를 추측할 수는 있지만 명백하게 검증하지 못했던 게 아쉬움으로 많이 남는 것 같습니다. 제가 아직 많이 부족하다는 증거이기 때문에 앞으로는 이런 아쉬움이 남지 않도록 더 발전하고 노력하겠습니다.
긴 글 읽어주셔서 감사합니다