이미지 생성 AI를 오프라인 행사에 사용하기 (with GENESIS)

안녕하세요. 서비스 및 소프트웨어 개발 아웃소싱 서비스를 제공하는 풀링포레스트 팀의 송찬영입니다.

우리는 스테이블 디퓨전을 오프라인 행사에 활용하고 싶다는 고객사의 의뢰를 받고 개발에 착수했습니다. 부산국제영화제 제네시스 홍보부스 안에서 사용될, 제네시스 차량이 등장하는 AI 포스터 생성 키오스크 제작 의뢰였습니다.

1. LORA 학습

핵심이 제네시스 차량 생성에 있는 만큼, 차량 이미지 학습을 우선적으로 진행했습니다. SDXL 모델을 기반으로, LORA 학습을 진행했습니다.

제네시스 차량 이미지 1

학습에 사용될 GPU 서버는 runpod에서 임대했습니다. runpod이 제공하는 Secure Cloud를 사용하면, 안전하면서 합리적인 가격에 GPU 서버를 임대할 수 있습니다. RTX 3090 수준의 GPU를 임대했으며, 시간당 0.44 달러의 비용을 지출했습니다. SDXL LORA 학습에는 24 GB VRAM 정도가 보장되어야 안정적인 학습이 가능합니다.

Run Pod의 Network Volume을 사용하면, 서버를 중간에 중단하고 다른 서버로 재임대하여도, 네트워크 볼륨만 동일하게 연결하면 데이터를 그대로 사용할 수 있습니다. 방대한 모델 데이터를 매번 다운로드하지 않아도 되는 편리함이 장점이었습니다. 또한 기본적으로 WebUI, ComfyUI가 설치된 컨테이너로 서버를 시작할 수 있기에, 초기 서버 세팅에 소요되는 시간도 단축되었습니다.

LORA 학습에는 kohya_ss/sd-scripts를 사용해 진행했습니다. 차량은 제네시스의 컨버터블 차량으로 확정되었습니다. (X Convertible)

제네시스 차량 이미지 2 제네시스 차량 이미지 3

학습에는 총 138장의 이미지를 사용했습니다. 이미지의 해상도가 통일될 필요는 없습니다. 다만 너무 작거나, 너무 큰 이미지에서 문제가 생길 수 있기 때문에, 최소 1024px / 최대 3000px로 사이즈를 조절했습니다. 일괄적인 사이즈 조절을 위해 파이썬 스크립트를 작성하여 사용했습니다.

kohya_ss/sd-scripts는 데이터셋 폴더명 규칙이 있습니다. 예를 들면 9_ohwx convertible처럼, 반복횟수_instancePrompt classPrompt로 구성됩니다.

instancePrompt(또는 identifier라고 불리는)는 학습 대상을 식별하기 위한 키워드로, 아무 의미가 없는 3문자 이하가 권장됩니다. (예: hs, sts, scs, cpc, coc, cic, msm, usu, ici, lvl, cic, dii, muk, ori, hru, rik, koo, yos, wny)

classPrompt는 실제 학습시킬 대상의 키워드입니다. 우리 같은 경우에는 car 또는 convertible과 같은 키워드가 되겠죠.

일반적으로 LORA 학습에는 정규화 이미지가 사용됩니다. 제네시스 컨버터블을 학습시킨다고 하면, 유사한 다른 차량들의 이미지를 정규화 이미지로 준비하고, 정규화 이미지를 사용하여 함께 학습시킬 때 결과물이 좋아지는 경향이 있습니다.

그러나 이번에는 정규화 이미지를 사용하지는 않았고, 캡션 방식을 사용했습니다.

캡션 이미지 1

캡션은 학습시킬 이미지와 동일한 파일 이름의 txt 파일을 만들어서, 이미지를 설명해주는 문장을 작성해주면 됩니다. 캡션의 맨 앞에는 사전에 정했던 instance prompt를 넣어줍니다. 정규화 이미지를 사용한다면, 정규화 이미지마다 캡션을 달아주어야 합니다. 아무튼 캡션을 사용했을 때 확연하게 결과물의 퀄리티 향상이 되었습니다.

그러나 다수의 사진에 캡션을 하나씩 다는 건 고된 일입니다. BLIP을 사용하여 자동 이미지 캡셔닝을 활용하는 방법도 있습니다. kohya_ss/sd-scripts의 루트 폴더에서 다음의 명령어를 실행하면, 해당 데이터셋 폴더 안의 이미지들을 인식하여 캡션 파일들을 만들어줍니다. 이제 우리는 개별적으로 확인하면서, 이상한 캡션들만 교정해주면 됩니다. (기타 옵션은 적절히 조정해도 됩니다)

python3 "finetune/make_captions.py" --batch_size="4" --num_beams="10" --top_p="0.9" --max_length="75" --min_length="22" --beam_search --caption_extension=".txt" "데이터셋 경로" --caption_weights="https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_large_caption.pth"

이미지 캡션이 완료되면 본격적인 LORA 학습을 진행할 수 있습니다. 저희가 최종적으로 사용한 옵션은 다음과 같습니다.

--network_alpha="1" --text_encoder_lr=0.0004 --unet_lr=0.0004 --network_dim=12 --lr_scheduler_num_cycles="10" --learning_rate="0.0004" --lr_scheduler="cosine_with_restarts" --train_batch_size="1" --max_train_epochs=10 --save_every_n_epochs="1" --mixed_precision="bf16" --save_precision="bf16" --caption_extension="txt" --cache_latents --cache_latents_to_disk --optimizer_type="Adafactor" --optimizer_args scale_parameter=False relative_step=False warmup_init=False --max_data_loader_n_workers="0" --bucket_reso_steps=64 --gradient_checkpointing --xformers --bucket_no_upscale --network_train_unet_only

참고로 train_batch_size x Epoch x Repeats가 총 스텝 수를 결정합니다.

옵션에 대한 자세한 정보는 여기를 참고하면 좋습니다.

학습 결과 이미지

결과적으로 학습은 잘 완료되었고, LORA를 적용해 준수한 이미지들을 뽑아내는 데 성공했습니다.

2. 프론트 / 백엔드 구축

이제 가장 중요한 AI 쪽의 고비를 넘겼으니, 프론트와 백만 잘 구축하면 됩니다.

피그마 디자인 초안

위 사진은 피그마로 만든 디자인 초안입니다. 향후 키오스크 환경에서의 사용자 경험 향상을 위해, 프롬프트를 직접 입력하는 방식에서, 몇 개의 키워드를 제시하고, 유저는 선택만 하면 되는 방식으로 변경되었습니다.

전반적인 구조

전반적인 구조는 위와 같습니다.

  1. Next.js의 자체 API 라우트를 활용하여, 유저가 프롬프트를 입력하면 구글 번역기 API로 영어로 변환하여 bullmq 대기열에 추가됩니다.

프론트(키오스크 화면)은 Next.js로 개발했으며, 프론트 배포에 신경 써야 하는 에너지를 줄이기 위해 Vercel을 통한 배포를 선택했습니다.

오프라인 키오스크 특성상, 여러 번 프롬프트와 파라미터를 바꿔가며 퀄리티 향상을 시킬 수 있는 환경이 아니었으므로, 누구나 대충 입력해도 알아서 잘 나오게 했어야 했습니다.

결과적으로 특정한 프롬프트를 미리 입력해놓고, 유저가 입력한 값이 사전 입력된 프롬프트의 변수로 들어가게끔 했습니다.

같은 이유로, 네거티브 프롬프트가 매우 중요했습니다. 특히 비정상적인 사진, 다른 차량이 나오는 확률을 낮추는 게 가장 중요했습니다. 물론 유저가 네거티브 프롬프트를 입력할 필요는 없었고, 다수의 실험을 통해 사전에 미리 정해놓았습니다.

SDXL 기본 모델은 숙련된 사용자가 프롬프트와 파라미터를 바꿔가며 생성했을 때 좋은 결과를 뽑아냈지만, 키오스크 환경에서 일정한 키워드 조합만으로 높은 퀄리티를 기대하기는 어려웠습니다. 결국 ProtoVision XL 모델을 사용했습니다. 대충 입력해도 대비가 강하고 애니메이션 풍의 그럴듯한 이미지를 잘 생성해내었습니다. 결과적으로 최종 퀄리티 상승에는 베이스가 되는 모델이 매우 중요했습니다.

  1. bullmq 사용을 위한 redis 서버는 redis 클라우드를 사용하여 간편하게 연동 완료하였습니다.

  2. 대기열에 추가되면 jobIds를 받아 웹소켓 서버에 연결했습니다. 웹소켓 서버는 GPU 서버로부터 이미지를 수신하면, 클라이언트(키오스크)에 송신하는 역할을 합니다.

  3. 저는 GPU 이미지 생성 서버를 하나의 도커 컨테이너로 만들고, 수요에 따라 유연하게 늘려나가면 좋겠다는 생각을 했습니다. 결국 Docker Compose 기반으로, express.js 기반의 API 서버(대기열을 감지하고 스테이블 디퓨전에 요청)와 ComfyUI를 묶어 구축했습니다.

WebUI와 ComfyUI 모두 API 기능을 제공합니다. 다만 SDXL + Lora 조합에서는 ComfyUI가 훨씬 안정적이고 속도가 빨랐습니다.

ComfyUI가 이미지를 생성하면, Express.js는 해당 이미지 파일을 S3에 업로드하고, S3 경로를 웹소켓 서버로 전달하며, 웹소켓 서버는 해당 이미지 URL을 키오스크로 전달합니다.

GPU 서버는 AWS g4dn.xlarge 인스턴스를 사용했으며, 행사 기간 중 3~5개의 인스턴스를 사용했습니다.

  1. 최종적으로 키오스크에서 유저는, 스테이블 디퓨전이 생성한 3개의 이미지 중 한 개의 이미지를 선택하고 텍스트와 컬러를 넣어 포스터를 꾸민 뒤 인쇄하게 됩니다.

  2. DB는 단순히 로그 기록, 통계용으로 사용했으며, 간단하고 저렴한 PlanetScale을 사용했습니다.

3. 키오스크 구축

크롬 kiosk 모드로 실행하면 될 것이라 안일하게 생각했으나, 어떠한 방법을 사용해도 시스템 프린트 대화상자가 출력되는 문제가 있었습니다.

결국 일렉트론으로 윈도우 데스크탑 앱을 만들고, 일렉트론 내의 웹뷰로 Vercel 프론트를 띄우고, 일렉트론의 프린트 기능과 연동했습니다.

여러 가지 시도를 했으나, 다음의 방법으로 프린트 대화상자 없이, 스무스하게 출력에 성공했습니다.

  1. Next.js에서 유저가 최종적으로 완성한 이미지를 S3에 올리고, S3 이미지 경로를 일렉트론으로 전송 (window.ipcRenderer.send("electron-print-command", finalImageUrl))

  2. 일렉트론에서 수신한 이미지 경로로 새로운 창을 열고, 해당 창을 프린트

키오스크 출력


실제 행사는 아래와 같이 진행되었습니다. 주말에는 대기열이 길어 GPU 서버도 증설해야 했습니다. AI 이미지 생성이 온라인을 넘어 오프라인에서도 화제성을 만들어낼 수 있었습니다.

행사 사진 1 행사 사진 2 행사 사진 3

재미있게 읽으셨나요?

우리는 비즈니스와 기술을 연결해

산업을 혁신하는 서비스를 만들고있어요.

우리가 궁금하다면 아래 버튼을 눌러주세요.

빠른 상담 요청Contact MePoooling Forest Contact System