์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽจ CLIP ๋ชจ๋ธ๋กœ ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๋ฅผ ์ž์œ ์ž์žฌ๋กœ ๋งค์นญํ•˜๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๊ธฐ์ˆ 

๐ŸŽจ CLIP ๋ชจ๋ธ๋กœ ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๋ฅผ ์ž์œ ์ž์žฌ๋กœ ๋งค์นญํ•˜๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๊ธฐ์ˆ 

OpenAI๊ฐ€ ์„ ๋ณด์ธ ํ˜์‹ ์ ์ธ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI, ๊ทธ ์›๋ฆฌ๋ถ€ํ„ฐ ์‹ค์ „ ๊ตฌํ˜„๊นŒ์ง€ ์™„๋ฒฝ ๊ฐ€์ด๋“œ ๐Ÿš€

๐ŸŒŸ CLIP, ๋Œ€์ฒด ๋ญ๊ธธ๋ž˜ ์ด๋ ‡๊ฒŒ ๋‚œ๋ฆฌ์•ผ?

์•ˆ๋…•, ์นœ๊ตฌ! ๐Ÿ˜Š ์˜ค๋Š˜์€ ์ •๋ง ์‹ ๋‚˜๋Š” ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๋ฐ”๋กœ CLIP ๋ชจ๋ธ์ด๋ผ๋Š” ๋…€์„์ธ๋ฐ, ์ด๊ฒŒ ์ง„์งœ ํ˜๋ช…์ ์ด์•ผ. ์ƒ์ƒํ•ด๋ด, ์ปดํ“จํ„ฐํ•œํ…Œ "๊ท€์—ฌ์šด ๊ฐ•์•„์ง€๊ฐ€ ๊ณต์›์—์„œ ๋›ฐ์–ด๋…ธ๋Š” ์‚ฌ์ง„"์ด๋ผ๊ณ  ๋งํ•˜๋ฉด, ์ˆ˜์ฒœ ์žฅ์˜ ์ด๋ฏธ์ง€ ์ค‘์—์„œ ๋”ฑ ๊ทธ ์žฅ๋ฉด์„ ์ฐพ์•„๋‚ด๋Š” ๊ฑฐ์•ผ! ๐Ÿ•โœจ

2021๋…„ ์ดˆ, OpenAI๊ฐ€ CLIP(Contrastive Language-Image Pre-training)์„ ๋ฐœํ‘œํ–ˆ์„ ๋•Œ AI ์ปค๋ฎค๋‹ˆํ‹ฐ๋Š” ์™„์ „ํžˆ ๋’ค์ง‘ํ˜”์–ด. ์™œ๋ƒ๊ณ ? ๊ธฐ์กด์˜ ์ด๋ฏธ์ง€ ์ธ์‹ ๋ชจ๋ธ๋“ค์€ "๊ฐœ", "๊ณ ์–‘์ด", "์ž๋™์ฐจ" ๊ฐ™์€ ์ •ํ•ด์ง„ ์นดํ…Œ๊ณ ๋ฆฌ๋งŒ ์ธ์‹ํ•  ์ˆ˜ ์žˆ์—ˆ๊ฑฐ๋“ . ๊ทผ๋ฐ CLIP์€ ์™„์ „ํžˆ ๋‹ค๋ฅธ ์ ‘๊ทผ์„ ํ–ˆ์–ด. ์ž์—ฐ์–ด๋กœ ๋œ ์„ค๋ช…๊ณผ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ํ•™์Šตํ•ด์„œ, ํ•œ ๋ฒˆ๋„ ๋ณธ ์  ์—†๋Š” ๊ฐœ๋…๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฑฐ์ง€! ๐ŸŽฏ

๐Ÿ’ก CLIP์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด "์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๋ฅผ ๊ฐ™์€ ๊ณต๊ฐ„์— ๋งคํ•‘ํ•˜๋ฉด ์–ด๋–จ๊นŒ?"

์ด๊ฒŒ ์ „๋ถ€์•ผ. ๊ฐ„๋‹จํ•˜์ง€? ํ•˜์ง€๋งŒ ์ด ๊ฐ„๋‹จํ•œ ์•„์ด๋””์–ด๊ฐ€ 4์–ต ๊ฐœ์˜ ์ด๋ฏธ์ง€-ํ…์ŠคํŠธ ์Œ์œผ๋กœ ํ•™์Šต๋˜๋ฉด์„œ ์—„์ฒญ๋‚œ ๋Šฅ๋ ฅ์„ ๊ฐ–๊ฒŒ ๋์–ด. ๋งˆ์น˜ ์‚ฌ๋žŒ์ด ์‚ฌ์ง„์„ ๋ณด๊ณ  ์„ค๋ช…์„ ๋“ฃ๋“ฏ์ด, CLIP๋„ ๊ทธ๋ ‡๊ฒŒ ๋ฐฐ์šด ๊ฑฐ์•ผ! ๐Ÿง 
Image Encoder Vision Transformer Text Encoder A cute dog playing in the park Transformer Embedding Space Contrastive Learning ๋งค์นญ๋˜๋Š” ์Œ์€ ๊ฐ€๊น๊ฒŒ, ๋‹ค๋ฅธ ์Œ์€ ๋ฉ€๊ฒŒ!

๐Ÿ” CLIP์˜ ์ž‘๋™ ์›๋ฆฌ: ๋งˆ๋ฒ• ๊ฐ™์ง€๋งŒ ๊ณผํ•™์ด์•ผ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ CLIP์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํŒŒํ—ค์ณ๋ณด์ž! ๐Ÿ•ต๏ธ ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค๋ฉด "์•„ํ•˜!" ํ•˜๋Š” ์ˆœ๊ฐ„์ด ์˜ฌ ๊ฑฐ์•ผ.

CLIP์˜ ํ•ต์‹ฌ์€ ๋‘ ๊ฐœ์˜ ์ธ์ฝ”๋”์•ผ. ํ•˜๋‚˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” ํ…์ŠคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ด. ๊ทธ๋ฆฌ๊ณ  ์ด ๋‘˜์ด ๋งŒ๋“ค์–ด๋‚ธ ๊ฒฐ๊ณผ๋ฌผ์„ ๊ฐ™์€ ๊ณต๊ฐ„(Embedding Space)์— ๋†“๋Š” ๊ฑฐ์ง€. ๋งˆ์น˜ ์„œ๋กœ ๋‹ค๋ฅธ ์–ธ์–ด๋ฅผ ์“ฐ๋Š” ์‚ฌ๋žŒ๋“ค์ด ๊ณตํ†ต์˜ ์–ธ์–ด๋กœ ๋Œ€ํ™”ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ง์ด์•ผ! ๐ŸŒ

๐Ÿ“ธ ์ด๋ฏธ์ง€ ์ธ์ฝ”๋”: ์‹œ๊ฐ ์ •๋ณด์˜ ๋ฒˆ์—ญ๊ฐ€

์ด๋ฏธ์ง€ ์ธ์ฝ”๋”๋Š” ์ฃผ๋กœ Vision Transformer(ViT)๋‚˜ ResNet ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ด. OpenAI์˜ ์›๋ณธ CLIP์€ ๋‘ ๊ฐ€์ง€ ๋ฒ„์ „์„ ์ œ๊ณตํ•˜๋Š”๋ฐ:

1. ResNet ๊ธฐ๋ฐ˜ (RN50, RN101 ๋“ฑ)
์ „ํ†ต์ ์ธ CNN ๊ตฌ์กฐ๋กœ, ์ด๋ฏธ์ง€๋ฅผ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ ํ•ฉ์„ฑ๊ณฑ ๋ ˆ์ด์–ด๋ฅผ ๊ฑฐ์ณ ์ฒ˜๋ฆฌํ•ด. ์•ˆ์ •์ ์ด๊ณ  ๊ฒ€์ฆ๋œ ๋ฐฉ์‹์ด์ง€! ๐Ÿ›๏ธ

2. Vision Transformer ๊ธฐ๋ฐ˜ (ViT-B/32, ViT-L/14 ๋“ฑ)
์ด๋ฏธ์ง€๋ฅผ ์ž‘์€ ํŒจ์น˜๋กœ ๋‚˜๋ˆ ์„œ Transformer๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์ตœ์‹  ๋ฐฉ์‹. ๋” ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ๊ณ„์‚ฐ๋Ÿ‰์ด ๋งŽ์•„. ๐Ÿ’ช

์–ด๋–ค ๊ตฌ์กฐ๋ฅผ ์“ฐ๋“ , ์ตœ์ข… ๋ชฉํ‘œ๋Š” ๊ฐ™์•„. ์ด๋ฏธ์ง€๋ฅผ ๊ณ ์ •๋œ ํฌ๊ธฐ์˜ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฑฐ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด, 512์ฐจ์› ๋ฒกํ„ฐ๋กœ ๋งŒ๋“ค๋ฉด, ๊ทธ ๋ฒกํ„ฐ๊ฐ€ ์ด๋ฏธ์ง€์˜ ๋ชจ๋“  ํŠน์ง•์„ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฑฐ์ง€!

๐ŸŽ“ ์ž ๊น ํŒ! ViT-B/32์—์„œ "32"๋Š” ํŒจ์น˜ ํฌ๊ธฐ๋ฅผ ์˜๋ฏธํ•ด. ์ด๋ฏธ์ง€๋ฅผ 32ร—32 ํ”ฝ์…€ ํŒจ์น˜๋กœ ๋‚˜๋ˆˆ๋‹ค๋Š” ๋œป์ด์•ผ. ์ˆซ์ž๊ฐ€ ์ž‘์„์ˆ˜๋ก ๋” ์„ธ๋ฐ€ํ•˜๊ฒŒ ๋ถ„์„ํ•˜์ง€๋งŒ, ๊ณ„์‚ฐ๋Ÿ‰๋„ ๋Š˜์–ด๋‚˜. ViT-L/14๊ฐ€ ViT-B/32๋ณด๋‹ค ์„ฑ๋Šฅ์ด ์ข‹์€ ์ด์œ ๊ฐ€ ๋ฐ”๋กœ ์ด๊ฑฐ์•ผ!

๐Ÿ“ ํ…์ŠคํŠธ ์ธ์ฝ”๋”: ์–ธ์–ด์˜ ๋งˆ๋ฒ•์‚ฌ

ํ…์ŠคํŠธ ์ธ์ฝ”๋”๋Š” Transformer ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•ด. GPT์™€ ๋น„์Šทํ•œ ๊ตฌ์กฐ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ! ํ…์ŠคํŠธ๋ฅผ ํ† ํฐ์œผ๋กœ ๋‚˜๋ˆ„๊ณ , ๊ฐ ํ† ํฐ์˜ ์˜๋ฏธ์™€ ๋ฌธ๋งฅ์„ ํŒŒ์•…ํ•ด์„œ ์ตœ์ข…์ ์œผ๋กœ ํ•˜๋‚˜์˜ ๋ฒกํ„ฐ๋กœ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๊ฑฐ์•ผ. ๐Ÿ“š

์žฌ๋ฏธ์žˆ๋Š” ๊ฑด, CLIP์˜ ํ…์ŠคํŠธ ์ธ์ฝ”๋”๋Š” ์ตœ๋Œ€ 77๊ฐœ์˜ ํ† ํฐ๊นŒ์ง€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ๊ฝค ๊ธด ๋ฌธ์žฅ๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด์ง€! "A photo of a golden retriever puppy playing with a red ball in a sunny park during autumn"์ฒ˜๋Ÿผ ๊ตฌ์ฒด์ ์ธ ์„ค๋ช…๋„ ๊ฐ€๋Šฅํ•ด. ๐Ÿ‚

๐ŸŽฏ ๋Œ€์กฐ ํ•™์Šต(Contrastive Learning): ํ•ต์‹ฌ ์ค‘์˜ ํ•ต์‹ฌ

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด์•ผ! CLIP์ด ์–ด๋–ป๊ฒŒ ํ•™์Šตํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž. ๐ŸŽ“

์ƒ์ƒํ•ด๋ด. ๋ฐฐ์น˜(batch) ์•ˆ์— 32๊ฐœ์˜ ์ด๋ฏธ์ง€-ํ…์ŠคํŠธ ์Œ์ด ์žˆ์–ด. ๊ฐ ์ด๋ฏธ์ง€๋Š” ์ž์‹ ๊ณผ ๋งค์นญ๋˜๋Š” ํ…์ŠคํŠธ๊ฐ€ ํ•˜๋‚˜์”ฉ ์žˆ์ง€. CLIP์˜ ๋ชฉํ‘œ๋Š” ๋ญ˜๊นŒ?

๋งค์นญ๋˜๋Š” ์ด๋ฏธ์ง€-ํ…์ŠคํŠธ ์Œ์˜ ๋ฒกํ„ฐ๋Š” ๊ฐ€๊น๊ฒŒ, ๋งค์นญ๋˜์ง€ ์•Š๋Š” ์Œ์˜ ๋ฒกํ„ฐ๋Š” ๋ฉ€๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑฐ์•ผ! ๐ŸŽฏ

๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…ํ•˜๋ฉด:

๋Œ€์กฐ ํ•™์Šต์˜ ๊ณผ์ • 1๏ธโƒฃ 32๊ฐœ ์ด๋ฏธ์ง€๋ฅผ ์ด๋ฏธ์ง€ ์ธ์ฝ”๋”์— ๋„ฃ์–ด โ†’ 32๊ฐœ์˜ ์ด๋ฏธ์ง€ ๋ฒกํ„ฐ ์ƒ์„ฑ
2๏ธโƒฃ 32๊ฐœ ํ…์ŠคํŠธ๋ฅผ ํ…์ŠคํŠธ ์ธ์ฝ”๋”์— ๋„ฃ์–ด โ†’ 32๊ฐœ์˜ ํ…์ŠคํŠธ ๋ฒกํ„ฐ ์ƒ์„ฑ
3๏ธโƒฃ 32ร—32 = 1,024๊ฐœ์˜ ๊ฐ€๋Šฅํ•œ ์กฐํ•ฉ ์ค‘์—์„œ
4๏ธโƒฃ ์ •๋‹ต ์Œ 32๊ฐœ๋Š” ์œ ์‚ฌ๋„๋ฅผ ๋†’์ด๊ณ  (๋Œ€๊ฐ์„  ์š”์†Œ)
5๏ธโƒฃ ์˜ค๋‹ต ์Œ 992๊ฐœ๋Š” ์œ ์‚ฌ๋„๋ฅผ ๋‚ฎ์ถฐ! (๋น„๋Œ€๊ฐ์„  ์š”์†Œ)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ชจ๋ธ์ด "์–ด๋–ค ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๊ฐ€ ์„œ๋กœ ๊ด€๋ จ์žˆ๋Š”์ง€" ๋ฐฐ์šฐ๊ฒŒ ๋ผ! ๐Ÿง 

์ด ๊ณผ์ •์„ ์ˆ˜์‹์œผ๋กœ ํ‘œํ˜„ํ•˜๋ฉด InfoNCE Loss๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด "์ •๋‹ต์€ ํ™•๋ฅ ์„ ๋†’์ด๊ณ , ์˜ค๋‹ต์€ ํ™•๋ฅ ์„ ๋‚ฎ์ถฐ๋ผ"๋Š” ๊ฑฐ์•ผ. ๋งˆ์น˜ ๊ฐ๊ด€์‹ ์‹œํ—˜์—์„œ ์ •๋‹ต์„ ์ฐพ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ! โœ…

๐Ÿ’ป ์‹ค์ „ ๊ตฌํ˜„: ์ฝ”๋“œ๋กœ ๋งŒ๋‚˜๋Š” CLIP

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์ง„์งœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž! ๐Ÿš€ Python๊ณผ PyTorch๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ. ๊ฑฑ์ • ๋งˆ, ํ•˜๋‚˜ํ•˜๋‚˜ ์„ค๋ช…ํ•ด์ค„๊ฒŒ!

๐Ÿ”ง ํ™˜๊ฒฝ ์„ค์ •: ์ค€๋น„์šด๋™๋ถ€ํ„ฐ

๋จผ์ € ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์„ค์น˜ํ•ด์•ผ ํ•ด. ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ  ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด๋ด:

pip install torch torchvision
pip install ftfy regex tqdm
pip install git+https://github.com/openai/CLIP.git

ftfy๋Š” ํ…์ŠคํŠธ ์ •๊ทœํ™”๋ฅผ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ณ , regex๋Š” ์ •๊ทœํ‘œํ˜„์‹, tqdm์€ ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ๋ฅผ ์œ„ํ•œ ๊ฑฐ์•ผ. ๋งˆ์ง€๋ง‰ ์ค„์€ OpenAI์˜ ๊ณต์‹ CLIP ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•˜๋Š” ๋ช…๋ น์–ด์ง€! ๐Ÿ“ฆ

๐ŸŽฌ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•: Hello CLIP!

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž. ์ด๋ฏธ์ง€ ํ•˜๋‚˜์™€ ์—ฌ๋Ÿฌ ํ…์ŠคํŠธ ์ค‘์—์„œ ๊ฐ€์žฅ ์ž˜ ๋งค์นญ๋˜๋Š” ๊ฑธ ์ฐพ๋Š” ์ฝ”๋“œ์•ผ:

import torch
import clip
from PIL import Image

# ๋””๋ฐ”์ด์Šค ์„ค์ • (GPU ์žˆ์œผ๋ฉด GPU, ์—†์œผ๋ฉด CPU)
device = "cuda" if torch.cuda.is_available() else "cpu"

# CLIP ๋ชจ๋ธ ๋กœ๋“œ
model, preprocess = clip.load("ViT-B/32", device=device)

# ์ด๋ฏธ์ง€ ๋กœ๋“œ ๋ฐ ์ „์ฒ˜๋ฆฌ
image = preprocess(Image.open("dog.jpg")).unsqueeze(0).to(device)

# ํ›„๋ณด ํ…์ŠคํŠธ๋“ค
text_candidates = [
    "a photo of a dog",
    "a photo of a cat", 
    "a photo of a car",
    "a photo of a bird"
]

# ํ…์ŠคํŠธ ํ† ํฐํ™”
text = clip.tokenize(text_candidates).to(device)

# ์ถ”๋ก  (inference)
with torch.no_grad():
    image_features = model.encode_image(image)
    text_features = model.encode_text(text)
    
    # ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ
    logits_per_image, logits_per_text = model(image, text)
    probs = logits_per_image.softmax(dim=-1).cpu().numpy()

# ๊ฒฐ๊ณผ ์ถœ๋ ฅ
print("๊ฐ ํ…์ŠคํŠธ์˜ ํ™•๋ฅ :")
for i, candidate in enumerate(text_candidates):
    print(f"{candidate}: {probs[0][i]*100:.2f}%")

์ด ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, ๊ฐ ํ…์ŠคํŠธ๊ฐ€ ์ด๋ฏธ์ง€์™€ ์–ผ๋งˆ๋‚˜ ์ž˜ ๋งค์นญ๋˜๋Š”์ง€ ํ™•๋ฅ ๋กœ ๋ณด์—ฌ์ค˜! ๋งŒ์•ฝ dog.jpg๊ฐ€ ์ •๋ง ๊ฐ•์•„์ง€ ์‚ฌ์ง„์ด๋ผ๋ฉด, "a photo of a dog"๊ฐ€ 90% ์ด์ƒ์˜ ๋†’์€ ํ™•๋ฅ ์„ ๋ณด์ผ ๊ฑฐ์•ผ. ๐Ÿ•

๐Ÿ” ์ฝ”๋“œ ํ•ด์„ค unsqueeze(0): ๋ฐฐ์น˜ ์ฐจ์›์„ ์ถ”๊ฐ€ํ•ด. [3, 224, 224] โ†’ [1, 3, 224, 224]
torch.no_grad(): ๊ทธ๋ž˜๋””์–ธํŠธ ๊ณ„์‚ฐ์„ ๋„๋Š” ๊ฑฐ์•ผ. ์ถ”๋ก ํ•  ๋•Œ๋Š” ํ•„์š” ์—†๊ฑฐ๋“ !
softmax(dim=-1): ๋กœ์ง“ ๊ฐ’์„ ํ™•๋ฅ ๋กœ ๋ณ€ํ™˜ํ•ด. ๋ชจ๋“  ํ™•๋ฅ ์˜ ํ•ฉ์€ 1์ด ๋ผ!

๐Ÿ”Ž ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์ข€ ๋” ์‹ค์šฉ์ ์ธ ๊ฑธ ๋งŒ๋“ค์–ด๋ณด์ž! ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€ ์ค‘์—์„œ ํ…์ŠคํŠธ ์ฟผ๋ฆฌ์™€ ๊ฐ€์žฅ ์ž˜ ๋งž๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ฐพ๋Š” ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์ด์•ผ. ๋งˆ์น˜ ๊ตฌ๊ธ€ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰์ฒ˜๋Ÿผ! ๐Ÿ”

import os
import torch
import clip
from PIL import Image
import numpy as np

class ImageSearchEngine:
    def __init__(self, model_name="ViT-B/32"):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load(model_name, device=self.device)
        self.image_features = None
        self.image_paths = []
    
    def index_images(self, image_folder):
        """์ด๋ฏธ์ง€ ํด๋”์˜ ๋ชจ๋“  ์ด๋ฏธ์ง€๋ฅผ ์ธ๋ฑ์‹ฑ"""
        print(f"๐Ÿ“ {image_folder}์˜ ์ด๋ฏธ์ง€๋“ค์„ ์ธ๋ฑ์‹ฑ ์ค‘...")
        
        image_files = [f for f in os.listdir(image_folder) 
                      if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
        
        features_list = []
        
        for img_file in image_files:
            img_path = os.path.join(image_folder, img_file)
            image = self.preprocess(Image.open(img_path)).unsqueeze(0).to(self.device)
            
            with torch.no_grad():
                features = self.model.encode_image(image)
                features /= features.norm(dim=-1, keepdim=True)  # ์ •๊ทœํ™”
            
            features_list.append(features)
            self.image_paths.append(img_path)
        
        self.image_features = torch.cat(features_list, dim=0)
        print(f"โœ… {len(self.image_paths)}๊ฐœ ์ด๋ฏธ์ง€ ์ธ๋ฑ์‹ฑ ์™„๋ฃŒ!")
    
    def search(self, query, top_k=5):
        """ํ…์ŠคํŠธ ์ฟผ๋ฆฌ๋กœ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰"""
        text = clip.tokenize([query]).to(self.device)
        
        with torch.no_grad():
            text_features = self.model.encode_text(text)
            text_features /= text_features.norm(dim=-1, keepdim=True)
        
        # ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ
        similarity = (text_features @ self.image_features.T).squeeze(0)
        
        # ์ƒ์œ„ k๊ฐœ ๊ฒฐ๊ณผ
        top_indices = similarity.argsort(descending=True)[:top_k]
        
        results = []
        for idx in top_indices:
            results.append({
                'path': self.image_paths[idx],
                'score': similarity[idx].item()
            })
        
        return results

# ์‚ฌ์šฉ ์˜ˆ์‹œ
if __name__ == "__main__":
    engine = ImageSearchEngine()
    engine.index_images("./my_photos")
    
    query = "a beautiful sunset over the ocean"
    results = engine.search(query, top_k=3)
    
    print(f"\n๐Ÿ” ๊ฒ€์ƒ‰์–ด: '{query}'")
    print("=" * 50)
    for i, result in enumerate(results, 1):
        print(f"{i}. {result['path']}")
        print(f"   ์œ ์‚ฌ๋„: {result['score']:.4f}")

์ด ์ฝ”๋“œ๋Š” ์ง„์งœ ์‹ค์šฉ์ ์ด์•ผ! ๐ŸŽ‰ index_images() ๋ฉ”์„œ๋“œ๋กœ ์ด๋ฏธ์ง€๋“ค์„ ๋ฏธ๋ฆฌ ์ธ์ฝ”๋”ฉํ•ด๋‘๊ณ , search() ๋ฉ”์„œ๋“œ๋กœ ์›ํ•˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด. ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ€์žฅ ๋น„์Šทํ•œ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ๋Š” ๊ฑฐ์ง€!

ํŠนํžˆ ์ •๊ทœํ™”(normalization)๊ฐ€ ์ค‘์š”ํ•ด. ๋ฒกํ„ฐ๋ฅผ ์ •๊ทœํ™”ํ•˜๋ฉด ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„๋ฅผ ๋‚ด์ (dot product)์œผ๋กœ ๊ฐ„๋‹จํžˆ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ์ˆ˜ํ•™์ ์œผ๋กœ ํšจ์œจ์ ์ด์•ผ. ๐Ÿ“

๐ŸŽจ ์ œ๋กœ์ƒท ์ด๋ฏธ์ง€ ๋ถ„๋ฅ˜๊ธฐ

CLIP์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์ œ๋กœ์ƒท ๋ถ„๋ฅ˜(Zero-shot Classification)์•ผ! ํ•™์Šต ๋ฐ์ดํ„ฐ ์—†์ด๋„ ์ƒˆ๋กœ์šด ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ถ„๋ฅ˜ํ•  ์ˆ˜ ์žˆ์–ด. ์–ด๋–ป๊ฒŒ? ํ…์ŠคํŠธ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ค๋ช…ํ•˜๋ฉด ๋˜๊ฑฐ๋“ ! ๐ŸŽฏ

class ZeroShotClassifier:
    def __init__(self, model_name="ViT-B/32"):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load(model_name, device=self.device)
    
    def classify(self, image_path, categories, template="a photo of a {}"):
        """์ด๋ฏธ์ง€๋ฅผ ์ฃผ์–ด์ง„ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜"""
        # ์ด๋ฏธ์ง€ ๋กœ๋“œ
        image = self.preprocess(Image.open(image_path)).unsqueeze(0).to(self.device)
        
        # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ…์ŠคํŠธ ์ƒ์„ฑ
        texts = [template.format(cat) for cat in categories]
        text_tokens = clip.tokenize(texts).to(self.device)
        
        # ์ธ์ฝ”๋”ฉ
        with torch.no_grad():
            image_features = self.model.encode_image(image)
            text_features = self.model.encode_text(text_tokens)
            
            # ์ •๊ทœํ™”
            image_features /= image_features.norm(dim=-1, keepdim=True)
            text_features /= text_features.norm(dim=-1, keepdim=True)
            
            # ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ ๋ฐ ํ™•๋ฅ  ๋ณ€ํ™˜
            similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)
        
        # ๊ฒฐ๊ณผ ์ •๋ฆฌ
        results = []
        for i, category in enumerate(categories):
            results.append({
                'category': category,
                'probability': similarity[0, i].item()
            })
        
        # ํ™•๋ฅ  ์ˆœ์œผ๋กœ ์ •๋ ฌ
        results.sort(key=lambda x: x['probability'], reverse=True)
        return results

# ์‚ฌ์šฉ ์˜ˆ์‹œ
classifier = ZeroShotClassifier()

# ๋™๋ฌผ ๋ถ„๋ฅ˜
animal_categories = ["dog", "cat", "bird", "fish", "horse", "elephant"]
results = classifier.classify("animal.jpg", animal_categories)

print("๐Ÿพ ๋™๋ฌผ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ:")
for result in results:
    print(f"{result['category']}: {result['probability']*100:.2f}%")

# ๊ฐ์ • ๋ถ„๋ฅ˜ (๊ฐ™์€ ์ด๋ฏธ์ง€๋กœ!)
emotion_categories = ["happy", "sad", "angry", "surprised", "neutral"]
emotion_template = "a {} looking animal"
results = classifier.classify("animal.jpg", emotion_categories, emotion_template)

print("\n๐Ÿ˜Š ๊ฐ์ • ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ:")
for result in results:
    print(f"{result['category']}: {result['probability']*100:.2f}%")

์ด ์ฝ”๋“œ์˜ ๋ฉ‹์ง„ ์ ์€ template ํŒŒ๋ผ๋ฏธํ„ฐ์•ผ! ํ…œํ”Œ๋ฆฟ์„ ๋ฐ”๊พธ๋ฉด ๊ฐ™์€ ์ด๋ฏธ์ง€๋ฅผ ๋‹ค๋ฅธ ๊ด€์ ์—์„œ ๋ถ„๋ฅ˜ํ•  ์ˆ˜ ์žˆ์–ด. "a photo of a {}"๋Š” ์ผ๋ฐ˜์ ์ธ ๋ถ„๋ฅ˜์— ์ข‹๊ณ , "a {} looking animal"์€ ๊ฐ์ • ๋ถ„๋ฅ˜์— ์ ํ•ฉํ•˜์ง€. ์œ ์—ฐ์„ฑ์ด ์ •๋ง ์ข‹์•„! ๐ŸŽญ

CLIP ํ™œ์šฉ ํŒŒ์ดํ”„๋ผ์ธ Input Image or Text Preprocessing Resize, Normalize Tokenize Encoding ViT / ResNet Transformer Embedding 512-dim Vector Applications ๐Ÿ” Image Search ๐Ÿท๏ธ Zero-shot Classification ๐ŸŽจ Content Moderation ๐Ÿค– Visual Question Answering

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋” ๋น ๋ฅด๊ณ  ํšจ์œจ์ ์œผ๋กœ

CLIP์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ์ œ๋Œ€๋กœ ์ตœ์ ํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ๋А๋ฆด ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์ˆ˜์ฒœ, ์ˆ˜๋งŒ ๊ฐœ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” ์ตœ์ ํ™”๊ฐ€ ํ•„์ˆ˜์•ผ! ๐Ÿš€ ์‹ค์ „์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ํŒ๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ.

๐ŸŽฏ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ์†๋„ ์˜ฌ๋ฆฌ๊ธฐ

์ด๋ฏธ์ง€๋ฅผ ํ•˜๋‚˜์”ฉ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑด ๋น„ํšจ์œจ์ ์ด์•ผ. GPU๋Š” ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ์— ์ตœ์ ํ™”๋˜์–ด ์žˆ๊ฑฐ๋“ ! ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์†๋„๊ฐ€ ์—„์ฒญ ๋นจ๋ผ์ ธ. ๐Ÿ’จ

def batch_encode_images(model, preprocess, image_paths, batch_size=32, device="cuda"):
    """์ด๋ฏธ์ง€๋“ค์„ ๋ฐฐ์น˜๋กœ ์ธ์ฝ”๋”ฉ"""
    all_features = []
    
    for i in range(0, len(image_paths), batch_size):
        batch_paths = image_paths[i:i+batch_size]
        
        # ๋ฐฐ์น˜ ์ด๋ฏธ์ง€ ๋กœ๋“œ
        images = []
        for path in batch_paths:
            image = preprocess(Image.open(path))
            images.append(image)
        
        # ํ…์„œ๋กœ ๋ณ€ํ™˜
        batch_tensor = torch.stack(images).to(device)
        
        # ์ธ์ฝ”๋”ฉ
        with torch.no_grad():
            features = model.encode_image(batch_tensor)
            features /= features.norm(dim=-1, keepdim=True)
        
        all_features.append(features)
    
    return torch.cat(all_features, dim=0)

# ์‚ฌ์šฉ ์˜ˆ์‹œ
image_paths = ["img1.jpg", "img2.jpg", ..., "img1000.jpg"]
features = batch_encode_images(model, preprocess, image_paths, batch_size=64)

๋ฐฐ์น˜ ํฌ๊ธฐ๋Š” GPU ๋ฉ”๋ชจ๋ฆฌ์— ๋”ฐ๋ผ ์กฐ์ ˆํ•ด์•ผ ํ•ด. RTX 3090 ๊ฐ™์€ ๊ณ ์„ฑ๋Šฅ GPU๋Š” 64~128๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ผ๋ฐ˜ GPU๋Š” 16~32๊ฐ€ ์ ๋‹นํ•ด. ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ๋ฐฐ์น˜ ํฌ๊ธฐ๋ฅผ ์ค„์—ฌ๋ด! ๐ŸŽฎ

๐Ÿ’พ ํŠน์ง• ๋ฒกํ„ฐ ์บ์‹ฑ

๊ฐ™์€ ์ด๋ฏธ์ง€๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ธ์ฝ”๋”ฉํ•˜๋Š” ๊ฑด ๋‚ญ๋น„์•ผ! ํ•œ ๋ฒˆ ๊ณ„์‚ฐํ•œ ํŠน์ง• ๋ฒกํ„ฐ๋Š” ์ €์žฅํ•ด๋‘๊ณ  ์žฌ์‚ฌ์šฉํ•˜์ž. ๐Ÿ—„๏ธ

import pickle
import hashlib

class FeatureCache:
    def __init__(self, cache_file="feature_cache.pkl"):
        self.cache_file = cache_file
        self.cache = self._load_cache()
    
    def _load_cache(self):
        """์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ"""
        try:
            with open(self.cache_file, 'rb') as f:
                return pickle.load(f)
        except FileNotFoundError:
            return {}
    
    def _save_cache(self):
        """์บ์‹œ ํŒŒ์ผ ์ €์žฅ"""
        with open(self.cache_file, 'wb') as f:
            pickle.dump(self.cache, f)
    
    def _get_file_hash(self, file_path):
        """ํŒŒ์ผ ํ•ด์‹œ ๊ณ„์‚ฐ (์บ์‹œ ํ‚ค๋กœ ์‚ฌ์šฉ)"""
        with open(file_path, 'rb') as f:
            return hashlib.md5(f.read()).hexdigest()
    
    def get_feature(self, file_path):
        """์บ์‹œ์—์„œ ํŠน์ง• ๋ฒกํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ"""
        file_hash = self._get_file_hash(file_path)
        return self.cache.get(file_hash)
    
    def set_feature(self, file_path, feature):
        """ํŠน์ง• ๋ฒกํ„ฐ๋ฅผ ์บ์‹œ์— ์ €์žฅ"""
        file_hash = self._get_file_hash(file_path)
        self.cache[file_hash] = feature.cpu().numpy()
        self._save_cache()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
cache = FeatureCache()

def encode_with_cache(model, preprocess, image_path, device="cuda"):
    # ์บ์‹œ ํ™•์ธ
    cached_feature = cache.get_feature(image_path)
    if cached_feature is not None:
        print(f"โœ… ์บ์‹œ์—์„œ ๋กœ๋“œ: {image_path}")
        return torch.from_numpy(cached_feature).to(device)
    
    # ์บ์‹œ ์—†์œผ๋ฉด ์ƒˆ๋กœ ๊ณ„์‚ฐ
    print(f"๐Ÿ”„ ์ƒˆ๋กœ ๊ณ„์‚ฐ: {image_path}")
    image = preprocess(Image.open(image_path)).unsqueeze(0).to(device)
    with torch.no_grad():
        feature = model.encode_image(image)
        feature /= feature.norm(dim=-1, keepdim=True)
    
    # ์บ์‹œ์— ์ €์žฅ
    cache.set_feature(image_path, feature)
    return feature

์ด ๋ฐฉ์‹์€ ํŠนํžˆ ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ ์œ ์šฉํ•ด! ๊ฐ™์€ ์ด๋ฏธ์ง€๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ํ…Œ์ŠคํŠธํ•  ๋•Œ ์‹œ๊ฐ„์„ ์—„์ฒญ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . โฐ

๐Ÿ”ง ๋ชจ๋ธ ์–‘์žํ™”(Quantization)

๋ชจ๋ธ ํฌ๊ธฐ๋ฅผ ์ค„์ด๊ณ  ์ถ”๋ก  ์†๋„๋ฅผ ๋†’์ด๋ ค๋ฉด ์–‘์žํ™”๋ฅผ ๊ณ ๋ คํ•ด๋ด. FP32(32๋น„ํŠธ ๋ถ€๋™์†Œ์ˆ˜์ )๋ฅผ FP16์ด๋‚˜ INT8๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฑฐ์•ผ. ์ •ํ™•๋„๋Š” ์•ฝ๊ฐ„ ๋–จ์–ด์ง€์ง€๋งŒ, ์†๋„๋Š” 2~4๋ฐฐ ๋นจ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด! โšก

# FP16 (Half Precision) ์‚ฌ์šฉ
model, preprocess = clip.load("ViT-B/32", device="cuda")
model = model.half()  # FP16์œผ๋กœ ๋ณ€ํ™˜

# ์ด๋ฏธ์ง€๋„ FP16์œผ๋กœ
image = preprocess(Image.open("test.jpg")).unsqueeze(0).to("cuda").half()

with torch.no_grad():
    features = model.encode_image(image)
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ FP16์€ GPU์—์„œ๋งŒ ํšจ๊ณผ์ ์ด์•ผ. CPU์—์„œ๋Š” ์˜คํžˆ๋ ค ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด. ๊ทธ๋ฆฌ๊ณ  ์ผ๋ถ€ ์˜ค๋ž˜๋œ GPU๋Š” FP16์„ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋‹ˆ ํ™•์ธํ•ด๋ด!

๐ŸŽจ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋‚˜๋งŒ์˜ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ์›น์•ฑ ๋งŒ๋“ค๊ธฐ

์ด์ œ ๋ฐฐ์šด ๊ฑธ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์ž! Flask๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ์„œ๋น„์Šค๋ฅผ ๊ตฌ์ถ•ํ•  ๊ฑฐ์•ผ. ๐ŸŒ

๐Ÿ“ฆ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

image_search_app/
โ”œโ”€โ”€ app.py                 # Flask ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
โ”œโ”€โ”€ clip_engine.py         # CLIP ๊ฒ€์ƒ‰ ์—”์ง„
โ”œโ”€โ”€ templates/
โ”‚   โ””โ”€โ”€ index.html        # ์›น ์ธํ„ฐํŽ˜์ด์Šค
โ”œโ”€โ”€ static/
โ”‚   โ”œโ”€โ”€ uploads/          # ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€
โ”‚   โ””โ”€โ”€ style.css         # ์Šคํƒ€์ผ์‹œํŠธ
โ””โ”€โ”€ requirements.txt      # ์˜์กด์„ฑ ํŒจํ‚ค์ง€

๐Ÿ”ง clip_engine.py: ๊ฒ€์ƒ‰ ์—”์ง„ ์ฝ”์–ด

import torch
import clip
from PIL import Image
import os
import pickle

class CLIPSearchEngine:
    def __init__(self, model_name="ViT-B/32", cache_dir="./cache"):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        print(f"๐Ÿš€ ๋””๋ฐ”์ด์Šค: {self.device}")
        
        self.model, self.preprocess = clip.load(model_name, device=self.device)
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)
        
        self.image_features = None
        self.image_paths = []
        self._load_index()
    
    def _load_index(self):
        """์ €์žฅ๋œ ์ธ๋ฑ์Šค ๋กœ๋“œ"""
        index_path = os.path.join(self.cache_dir, "index.pkl")
        if os.path.exists(index_path):
            with open(index_path, 'rb') as f:
                data = pickle.load(f)
                self.image_features = data['features'].to(self.device)
                self.image_paths = data['paths']
            print(f"โœ… ์ธ๋ฑ์Šค ๋กœ๋“œ ์™„๋ฃŒ: {len(self.image_paths)}๊ฐœ ์ด๋ฏธ์ง€")
    
    def _save_index(self):
        """์ธ๋ฑ์Šค ์ €์žฅ"""
        index_path = os.path.join(self.cache_dir, "index.pkl")
        data = {
            'features': self.image_features.cpu(),
            'paths': self.image_paths
        }
        with open(index_path, 'wb') as f:
            pickle.dump(data, f)
        print("๐Ÿ’พ ์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
    
    def add_image(self, image_path):
        """์ƒˆ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€"""
        image = self.preprocess(Image.open(image_path)).unsqueeze(0).to(self.device)
        
        with torch.no_grad():
            features = self.model.encode_image(image)
            features /= features.norm(dim=-1, keepdim=True)
        
        if self.image_features is None:
            self.image_features = features
        else:
            self.image_features = torch.cat([self.image_features, features], dim=0)
        
        self.image_paths.append(image_path)
        self._save_index()
    
    def search_by_text(self, query, top_k=10):
        """ํ…์ŠคํŠธ๋กœ ๊ฒ€์ƒ‰"""
        if self.image_features is None:
            return []
        
        text = clip.tokenize([query]).to(self.device)
        
        with torch.no_grad():
            text_features = self.model.encode_text(text)
            text_features /= text_features.norm(dim=-1, keepdim=True)
        
        similarity = (text_features @ self.image_features.T).squeeze(0)
        top_indices = similarity.argsort(descending=True)[:top_k]
        
        results = []
        for idx in top_indices:
            results.append({
                'path': self.image_paths[idx],
                'score': similarity[idx].item()
            })
        
        return results
    
    def search_by_image(self, image_path, top_k=10):
        """์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰"""
        if self.image_features is None:
            return []
        
        image = self.preprocess(Image.open(image_path)).unsqueeze(0).to(self.device)
        
        with torch.no_grad():
            query_features = self.model.encode_image(image)
            query_features /= query_features.norm(dim=-1, keepdim=True)
        
        similarity = (query_features @ self.image_features.T).squeeze(0)
        top_indices = similarity.argsort(descending=True)[1:top_k+1]  # ์ž๊ธฐ ์ž์‹  ์ œ์™ธ
        
        results = []
        for idx in top_indices:
            results.append({
                'path': self.image_paths[idx],
                'score': similarity[idx].item()
            })
        
        return results

๐ŸŒ app.py: Flask ์›น ์„œ๋ฒ„

from flask import Flask, render_template, request, jsonify
import os
from werkzeug.utils import secure_filename
from clip_engine import CLIPSearchEngine

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './static/uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB ์ œํ•œ

os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

# CLIP ์—”์ง„ ์ดˆ๊ธฐํ™”
engine = CLIPSearchEngine()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/upload', methods=['POST'])
def upload_image():
    """์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฐ ์ธ๋ฑ์‹ฑ"""
    if 'file' not in request.files:
        return jsonify({'error': 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค'}), 400
    
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'ํŒŒ์ผ์ด ์„ ํƒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค'}), 400
    
    if file:
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
        # ์ธ๋ฑ์Šค์— ์ถ”๊ฐ€
        engine.add_image(filepath)
        
        return jsonify({
            'success': True,
            'message': '์ด๋ฏธ์ง€๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
            'path': filepath
        })

@app.route('/search/text', methods=['POST'])
def search_text():
    """ํ…์ŠคํŠธ ๊ฒ€์ƒ‰"""
    data = request.get_json()
    query = data.get('query', '')
    top_k = data.get('top_k', 10)
    
    results = engine.search_by_text(query, top_k)
    
    return jsonify({
        'results': results,
        'count': len(results)
    })

@app.route('/search/image', methods=['POST'])
def search_image():
    """์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰"""
    if 'file' not in request.files:
        return jsonify({'error': 'ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค'}), 400
    
    file = request.files['file']
    filename = secure_filename(file.filename)
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], 'query_' + filename)
    file.save(filepath)
    
    results = engine.search_by_image(filepath, top_k=10)
    
    return jsonify({
        'results': results,
        'count': len(results)
    })

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

์ด์ œ python app.py๋กœ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5000์— ์ ‘์†ํ•˜๋ฉด ๋ผ! ๐ŸŽ‰

์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ธ๋ฑ์‹ฑ๋˜๊ณ , ํ…์ŠคํŠธ๋‚˜ ์ด๋ฏธ์ง€๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด. ์‹ค์ œ ์„œ๋น„์Šค์— ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์•ผ! ๋ฌผ๋ก  ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ณด์•ˆ, ์—๋Ÿฌ ์ฒ˜๋ฆฌ, ์Šค์ผ€์ผ๋ง ๋“ฑ์„ ๋” ๊ณ ๋ คํ•ด์•ผ ํ•˜์ง€๋งŒ ๋ง์ด์•ผ. ๐Ÿ›ก๏ธ

๐ŸŽฏ ๊ณ ๊ธ‰ ํ™œ์šฉ: CLIP์˜ ๋ฌดํ•œํ•œ ๊ฐ€๋Šฅ์„ฑ

๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋‹ˆ, ์ด์ œ ๋” ์ฐฝ์˜์ ์ธ ํ™œ์šฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! CLIP์€ ์ •๋ง ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŒˆ

๐ŸŽญ ์ฝ˜ํ…์ธ  ๋ชจ๋”๋ ˆ์ด์…˜

CLIP์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ€์ ์ ˆํ•œ ์ฝ˜ํ…์ธ ๋ฅผ ์ž๋™์œผ๋กœ ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์–ด. ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๊ฑฐ์ง€!

class ContentModerator:
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load("ViT-B/32", device=self.device)
        
        # ๋ถ€์ ์ ˆํ•œ ์ฝ˜ํ…์ธ  ์นดํ…Œ๊ณ ๋ฆฌ
        self.inappropriate_categories = [
            "violent content",
            "explicit content",
            "hate symbols",
            "graphic violence"
        ]
        
        # ์ ์ ˆํ•œ ์ฝ˜ํ…์ธ  ์นดํ…Œ๊ณ ๋ฆฌ
        self.appropriate_categories = [
            "safe for work content",
            "family friendly image",
            "appropriate content",
            "general audience content"
        ]
    
    def check_image(self, image_path, threshold=0.3):
        """์ด๋ฏธ์ง€ ์ ์ ˆ์„ฑ ๊ฒ€์‚ฌ"""
        image = self.preprocess(Image.open(image_path)).unsqueeze(0).to(self.device)
        
        all_categories = self.inappropriate_categories + self.appropriate_categories
        texts = [f"a photo containing {cat}" for cat in all_categories]
        text_tokens = clip.tokenize(texts).to(self.device)
        
        with torch.no_grad():
            image_features = self.model.encode_image(image)
            text_features = self.model.encode_text(text_tokens)
            
            image_features /= image_features.norm(dim=-1, keepdim=True)
            text_features /= text_features.norm(dim=-1, keepdim=True)
            
            similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)
        
        # ๋ถ€์ ์ ˆํ•œ ์ฝ˜ํ…์ธ  ์ ์ˆ˜ ๊ณ„์‚ฐ
        inappropriate_score = similarity[0, :len(self.inappropriate_categories)].sum().item()
        
        result = {
            'is_safe': inappropriate_score < threshold,
            'inappropriate_score': inappropriate_score,
            'details': {}
        }
        
        for i, cat in enumerate(all_categories):
            result['details'][cat] = similarity[0, i].item()
        
        return result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
moderator = ContentModerator()
result = moderator.check_image("user_upload.jpg")

if result['is_safe']:
    print("โœ… ์•ˆ์ „ํ•œ ์ฝ˜ํ…์ธ ์ž…๋‹ˆ๋‹ค")
else:
    print(f"โš ๏ธ ๋ถ€์ ์ ˆํ•œ ์ฝ˜ํ…์ธ  ๊ฐ€๋Šฅ์„ฑ: {result['inappropriate_score']:.2%}")

๐Ÿท๏ธ ์ž๋™ ํƒœ๊ทธ ์ƒ์„ฑ

์ด๋ฏธ์ง€์— ์ž๋™์œผ๋กœ ํƒœ๊ทธ๋ฅผ ๋‹ฌ์•„์ฃผ๋Š” ์‹œ์Šคํ…œ๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ์ˆ˜๋ฐฑ ๊ฐœ์˜ ํƒœ๊ทธ ํ›„๋ณด ์ค‘์—์„œ ๊ฐ€์žฅ ๊ด€๋ จ์„ฑ ๋†’์€ ํƒœ๊ทธ๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฑฐ์•ผ. ๐Ÿ“ธ

class AutoTagger:
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load("ViT-B/32", device=self.device)
        
        # ํƒœ๊ทธ ํ›„๋ณด๋“ค (์‹ค์ œ๋กœ๋Š” ๋” ๋งŽ์ด!)
        self.tag_candidates = [
            # ๊ฐ์ฒด
            "person", "people", "crowd", "dog", "cat", "car", "building",
            "tree", "flower", "food", "phone", "computer", "book",
            # ์žฅ์†Œ
            "indoor", "outdoor", "city", "nature", "beach", "mountain",
            "park", "street", "room", "office", "restaurant",
            # ํ™œ๋™
            "working", "playing", "eating", "sleeping", "running",
            "reading", "writing", "cooking", "driving",
            # ๋ถ„์œ„๊ธฐ
            "happy", "sad", "peaceful", "energetic", "romantic",
            "professional", "casual", "formal",
            # ์‹œ๊ฐ„/๋‚ ์”จ
            "daytime", "nighttime", "sunny", "cloudy", "rainy", "snowy",
            # ์Šคํƒ€์ผ
            "colorful", "black and white", "vintage", "modern",
            "minimalist", "artistic", "realistic"
        ]
    
    def generate_tags(self, image_path, top_k=10, threshold=0.2):
        """์ด๋ฏธ์ง€์— ๋Œ€ํ•œ ํƒœ๊ทธ ์ƒ์„ฑ"""
        image = self.preprocess(Image.open(image_path)).unsqueeze(0).to(self.device)
        
        # ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ํšจ์œจ์„ฑ ํ–ฅ์ƒ
        batch_size = 50
        all_similarities = []
        
        for i in range(0, len(self.tag_candidates), batch_size):
            batch_tags = self.tag_candidates[i:i+batch_size]
            texts = [f"a photo of {tag}" for tag in batch_tags]
            text_tokens = clip.tokenize(texts).to(self.device)
            
            with torch.no_grad():
                image_features = self.model.encode_image(image)
                text_features = self.model.encode_text(text_tokens)
                
                image_features /= image_features.norm(dim=-1, keepdim=True)
                text_features /= text_features.norm(dim=-1, keepdim=True)
                
                similarity = (image_features @ text_features.T).squeeze(0)
                all_similarities.append(similarity)
        
        # ๋ชจ๋“  ์œ ์‚ฌ๋„ ํ•ฉ์น˜๊ธฐ
        all_similarities = torch.cat(all_similarities)
        
        # ์ž„๊ณ„๊ฐ’ ์ด์ƒ์ธ ํƒœ๊ทธ๋งŒ ์„ ํƒ
        valid_indices = (all_similarities > threshold).nonzero(as_tuple=True)[0]
        
        if len(valid_indices) == 0:
            # ์ž„๊ณ„๊ฐ’์„ ๋งŒ์กฑํ•˜๋Š” ํƒœ๊ทธ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์œ„ k๊ฐœ ์„ ํƒ
            top_indices = all_similarities.argsort(descending=True)[:top_k]
        else:
            # ์ž„๊ณ„๊ฐ’ ์ด์ƒ ์ค‘์—์„œ ์ƒ์œ„ k๊ฐœ
            valid_similarities = all_similarities[valid_indices]
            sorted_valid = valid_similarities.argsort(descending=True)[:top_k]
            top_indices = valid_indices[sorted_valid]
        
        tags = []
        for idx in top_indices:
            tags.append({
                'tag': self.tag_candidates[idx],
                'confidence': all_similarities[idx].item()
            })
        
        return tags

# ์‚ฌ์šฉ ์˜ˆ์‹œ
tagger = AutoTagger()
tags = tagger.generate_tags("photo.jpg", top_k=15)

print("๐Ÿท๏ธ ์ƒ์„ฑ๋œ ํƒœ๊ทธ:")
for tag_info in tags:
    print(f"  #{tag_info['tag']} (์‹ ๋ขฐ๋„: {tag_info['confidence']:.3f})")

๐ŸŽจ ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ ๋ถ„์„

CLIP์œผ๋กœ ์ด๋ฏธ์ง€์˜ ์˜ˆ์ˆ ์  ์Šคํƒ€์ผ์ด๋‚˜ ๋ถ„์œ„๊ธฐ๋ฅผ ๋ถ„์„ํ•  ์ˆ˜๋„ ์žˆ์–ด! ์‚ฌ์ง„์ž‘๊ฐ€๋‚˜ ๋””์ž์ด๋„ˆ์—๊ฒŒ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ์ด์ง€. ๐Ÿ–ผ๏ธ

class StyleAnalyzer:
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load("ViT-B/32", device=self.device)
        
        self.style_dimensions = {
            'color_palette': [
                "vibrant colors", "muted colors", "pastel colors",
                "monochrome", "warm tones", "cool tones"
            ],
            'composition': [
                "symmetrical composition", "asymmetrical composition",
                "rule of thirds", "centered subject", "minimalist composition"
            ],
            'lighting': [
                "natural lighting", "artificial lighting", "dramatic lighting",
                "soft lighting", "harsh shadows", "golden hour lighting"
            ],
            'mood': [
                "cheerful mood", "melancholic mood", "mysterious mood",
                "energetic mood", "calm mood", "romantic mood"
            ],
            'art_style': [
                "realistic style", "abstract style", "impressionist style",
                "modern style", "vintage style", "cinematic style"
            ]
        }
    
    def analyze(self, image_path):
        """์ด๋ฏธ์ง€ ์Šคํƒ€์ผ ์ข…ํ•ฉ ๋ถ„์„"""
        image = self.preprocess(Image.open(image_path)).unsqueeze(0).to(self.device)
        
        analysis_result = {}
        
        for dimension, options in self.style_dimensions.items():
            texts = [f"a photo with {opt}" for opt in options]
            text_tokens = clip.tokenize(texts).to(self.device)
            
            with torch.no_grad():
                image_features = self.model.encode_image(image)
                text_features = self.model.encode_text(text_tokens)
                
                image_features /= image_features.norm(dim=-1, keepdim=True)
                text_features /= text_features.norm(dim=-1, keepdim=True)
                
                similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)
            
            # ๊ฐ€์žฅ ๋†’์€ ์ ์ˆ˜์˜ ์Šคํƒ€์ผ ์„ ํƒ
            top_idx = similarity.argmax().item()
            
            analysis_result[dimension] = {
                'primary': options[top_idx],
                'confidence': similarity[0, top_idx].item(),
                'all_scores': {opt: similarity[0, i].item() 
                              for i, opt in enumerate(options)}
            }
        
        return analysis_result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
analyzer = StyleAnalyzer()
analysis = analyzer.analyze("artwork.jpg")

print("๐ŸŽจ ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ ๋ถ„์„ ๊ฒฐ๊ณผ:\n")
for dimension, result in analysis.items():
    print(f"๐Ÿ“Š {dimension.replace('_', ' ').title()}:")
    print(f"   ์ฃผ์š” ํŠน์ง•: {result['primary']} ({result['confidence']:.1%})")
    print()

๐Ÿš€ CLIP์˜ ํ•œ๊ณ„์™€ ๊ทน๋ณต ๋ฐฉ๋ฒ•

CLIP์ด ์•„๋ฌด๋ฆฌ ๊ฐ•๋ ฅํ•ด๋„ ์™„๋ฒฝํ•˜์ง„ ์•Š์•„. ํ•œ๊ณ„๋ฅผ ์•Œ๊ณ  ์žˆ์–ด์•ผ ๋” ์ž˜ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์ง€! ๐ŸŽฏ

โš ๏ธ ์ฃผ์š” ํ•œ๊ณ„์ ๋“ค

1. ์„ธ๋ฐ€ํ•œ ๋””ํ…Œ์ผ ์ธ์‹์˜ ์–ด๋ ค์›€ CLIP์€ ์ „์ฒด์ ์ธ ๊ฐœ๋…์€ ์ž˜ ์ดํ•ดํ•˜์ง€๋งŒ, ์ž‘์€ ํ…์ŠคํŠธ๋‚˜ ์ˆซ์ž ๊ฐ™์€ ์„ธ๋ถ€ ์‚ฌํ•ญ์€ ์ž˜ ๋ชป ์ฝ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด, "์ˆซ์ž 7์ด ์ ํžŒ ํ‹ฐ์…”์ธ "์™€ "์ˆซ์ž 9๊ฐ€ ์ ํžŒ ํ‹ฐ์…”์ธ "๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์–ด๋ ค์›Œ. ๐Ÿ”

ํ•ด๊ฒฐ์ฑ…: OCR(๊ด‘ํ•™ ๋ฌธ์ž ์ธ์‹)์ด๋‚˜ ๊ฐ์ฒด ํƒ์ง€ ๋ชจ๋ธ๊ณผ ๊ฒฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•ด!
2. ์ถ”์ƒ์  ๊ฐœ๋…์˜ ์ดํ•ด ํ•œ๊ณ„ "ํ–‰๋ณตํ•ด ๋ณด์ด๋Š” ์‚ฌ๋žŒ"์€ ์ž˜ ์ฐพ์ง€๋งŒ, "์‹ค์กด์ฃผ์˜์  ๋ถ„์œ„๊ธฐ์˜ ์‚ฌ์ง„"์ฒ˜๋Ÿผ ์ถ”์ƒ์ ์ด๊ณ  ์ฒ ํ•™์ ์ธ ๊ฐœ๋…์€ ์–ด๋ ค์›Œํ•ด. ๐Ÿค”

ํ•ด๊ฒฐ์ฑ…: ์ถ”์ƒ์  ๊ฐœ๋…์„ ๊ตฌ์ฒด์ ์ธ ์‹œ๊ฐ์  ํŠน์ง•์œผ๋กœ ํ’€์–ด์„œ ์„ค๋ช…ํ•ด. "์–ด๋‘์šด ์กฐ๋ช…๊ณผ ๊ณ ๋…ํ•œ ์ธ๋ฌผ์ด ์žˆ๋Š” ์‚ฌ์ง„"์ฒ˜๋Ÿผ!
3. ๊ณต๊ฐ„์  ๊ด€๊ณ„ ์ดํ•ด ๋ถ€์กฑ "๊ฐœ ์™ผ์ชฝ์— ์žˆ๋Š” ๊ณ ์–‘์ด"์™€ "๊ฐœ ์˜ค๋ฅธ์ชฝ์— ์žˆ๋Š” ๊ณ ์–‘์ด"๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์–ด๋ ค์›Œ. ์œ„์น˜ ๊ด€๊ณ„๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ•ด. ๐Ÿ“

ํ•ด๊ฒฐ์ฑ…: ๊ฐ์ฒด ํƒ์ง€ ๋ชจ๋ธ๋กœ ๋จผ์ € ์œ„์น˜๋ฅผ ํŒŒ์•…ํ•œ ํ›„, CLIP์œผ๋กœ ๊ฐ์ฒด๋ฅผ ๋ถ„๋ฅ˜ํ•˜๋Š” ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•ด!
4. ๊ณ„์‚ฐ ๋น„์šฉ ๋Œ€๊ทœ๋ชจ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ํ•˜๋ ค๋ฉด ์ƒ๋‹นํ•œ ๊ณ„์‚ฐ ์ž์›์ด ํ•„์š”ํ•ด. ๐Ÿ’ฐ

ํ•ด๊ฒฐ์ฑ…: - ํŠน์ง• ๋ฒกํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด์„œ ์ €์žฅ (์˜คํ”„๋ผ์ธ ์ธ๋ฑ์‹ฑ) - FAISS ๊ฐ™์€ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ - ๋” ์ž‘์€ ๋ชจ๋ธ ์‚ฌ์šฉ (ViT-B/32 ๋Œ€์‹  RN50)

๐Ÿ”ง ์„ฑ๋Šฅ ๊ฐœ์„  ํŒ

๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• ํšจ๊ณผ
๋А๋ฆฐ ๊ฒ€์ƒ‰ ์†๋„ FAISS ๋ฒกํ„ฐ ์ธ๋ฑ์Šค ์‚ฌ์šฉ 10~100๋ฐฐ ๋น ๋ฆ„
๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ ๋ฐฐ์น˜ ํฌ๊ธฐ ์ค„์ด๊ธฐ, FP16 ์‚ฌ์šฉ ๋ฉ”๋ชจ๋ฆฌ 50% ์ ˆ๊ฐ
๋‚ฎ์€ ์ •ํ™•๋„ ๋” ํฐ ๋ชจ๋ธ ์‚ฌ์šฉ (ViT-L/14) ์ •ํ™•๋„ 5~10% ํ–ฅ์ƒ
๋„๋ฉ”์ธ ํŠนํ™” ์„ฑ๋Šฅ ํŒŒ์ธํŠœ๋‹ (Fine-tuning) ํŠน์ • ๋„๋ฉ”์ธ 20~30% ํ–ฅ์ƒ

๐ŸŒ FAISS์™€ ๊ฒฐํ•ฉ: ๋Œ€๊ทœ๋ชจ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ

์ˆ˜๋ฐฑ๋งŒ ๊ฐœ์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋ ค๋ฉด FAISS(Facebook AI Similarity Search)๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด! ๋ฒกํ„ฐ ๊ฒ€์ƒ‰์— ์ตœ์ ํ™”๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ๐Ÿš„

import faiss
import numpy as np

class ScalableImageSearch:
    def __init__(self, model_name="ViT-B/32", dimension=512):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load(model_name, device=self.device)
        self.dimension = dimension
        
        # FAISS ์ธ๋ฑ์Šค ์ดˆ๊ธฐํ™” (์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„์šฉ)
        self.index = faiss.IndexFlatIP(dimension)  # Inner Product (์ •๊ทœํ™”๋œ ๋ฒกํ„ฐ๋ฉด ์ฝ”์‚ฌ์ธ)
        self.image_paths = []
    
    def add_images_batch(self, image_paths, batch_size=32):
        """๋Œ€๋Ÿ‰์˜ ์ด๋ฏธ์ง€๋ฅผ ๋ฐฐ์น˜๋กœ ์ถ”๊ฐ€"""
        print(f"๐Ÿ“ฅ {len(image_paths)}๊ฐœ ์ด๋ฏธ์ง€ ์ธ๋ฑ์‹ฑ ์‹œ์ž‘...")
        
        for i in range(0, len(image_paths), batch_size):
            batch_paths = image_paths[i:i+batch_size]
            
            # ๋ฐฐ์น˜ ์ด๋ฏธ์ง€ ๋กœ๋“œ
            images = []
            valid_paths = []
            
            for path in batch_paths:
                try:
                    image = self.preprocess(Image.open(path))
                    images.append(image)
                    valid_paths.append(path)
                except Exception as e:
                    print(f"โš ๏ธ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: {path}")
                    continue
            
            if not images:
                continue
            
            # ๋ฐฐ์น˜ ์ธ์ฝ”๋”ฉ
            batch_tensor = torch.stack(images).to(self.device)
            
            with torch.no_grad():
                features = self.model.encode_image(batch_tensor)
                features /= features.norm(dim=-1, keepdim=True)
            
            # FAISS์— ์ถ”๊ฐ€ (numpy ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜)
            features_np = features.cpu().numpy().astype('float32')
            self.index.add(features_np)
            self.image_paths.extend(valid_paths)
            
            if (i + batch_size) % 1000 == 0:
                print(f"  ์ง„ํ–‰: {i + batch_size}/{len(image_paths)}")
        
        print(f"โœ… ์ธ๋ฑ์‹ฑ ์™„๋ฃŒ! ์ด {len(self.image_paths)}๊ฐœ ์ด๋ฏธ์ง€")
    
    def search(self, query, top_k=10, query_type='text'):
        """๊ฒ€์ƒ‰ (ํ…์ŠคํŠธ ๋˜๋Š” ์ด๋ฏธ์ง€)"""
        if query_type == 'text':
            text = clip.tokenize([query]).to(self.device)
            with torch.no_grad():
                query_features = self.model.encode_text(text)
                query_features /= query_features.norm(dim=-1, keepdim=True)
        else:  # image
            image = self.preprocess(Image.open(query)).unsqueeze(0).to(self.device)
            with torch.no_grad():
                query_features = self.model.encode_image(image)
                query_features /= query_features.norm(dim=-1, keepdim=True)
        
        # FAISS ๊ฒ€์ƒ‰
        query_np = query_features.cpu().numpy().astype('float32')
        distances, indices = self.index.search(query_np, top_k)
        
        results = []
        for i, idx in enumerate(indices[0]):
            if idx < len(self.image_paths):  # ์œ ํšจํ•œ ์ธ๋ฑ์Šค์ธ์ง€ ํ™•์ธ
                results.append({
                    'path': self.image_paths[idx],
                    'score': float(distances[0][i])
                })
        
        return results
    
    def save_index(self, filepath):
        """์ธ๋ฑ์Šค ์ €์žฅ"""
        faiss.write_index(self.index, f"{filepath}.faiss")
        with open(f"{filepath}.paths", 'wb') as f:
            pickle.dump(self.image_paths, f)
        print(f"๐Ÿ’พ ์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ: {filepath}")
    
    def load_index(self, filepath):
        """์ธ๋ฑ์Šค ๋กœ๋“œ"""
        self.index = faiss.read_index(f"{filepath}.faiss")
        with open(f"{filepath}.paths", 'rb') as f:
            self.image_paths = pickle.load(f)
        print(f"๐Ÿ“‚ ์ธ๋ฑ์Šค ๋กœ๋“œ ์™„๋ฃŒ: {len(self.image_paths)}๊ฐœ ์ด๋ฏธ์ง€")

# ์‚ฌ์šฉ ์˜ˆ์‹œ
search_engine = ScalableImageSearch()

# ๋Œ€๋Ÿ‰ ์ด๋ฏธ์ง€ ์ธ๋ฑ์‹ฑ
import glob
all_images = glob.glob("./image_dataset/**/*.jpg", recursive=True)
search_engine.add_images_batch(all_images, batch_size=64)

# ์ธ๋ฑ์Šค ์ €์žฅ
search_engine.save_index("./my_image_index")

# ๋‚˜์ค‘์— ๋กœ๋“œ
# search_engine.load_index("./my_image_index")

# ๊ฒ€์ƒ‰
results = search_engine.search("a beautiful sunset", top_k=20)
for i, result in enumerate(results, 1):
    print(f"{i}. {result['path']} (์œ ์‚ฌ๋„: {result['score']:.4f})")

FAISS๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐฑ๋งŒ ๊ฐœ ์ด๋ฏธ์ง€์—์„œ๋„ ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿš€ ์‹ค์ œ ์„œ๋น„์Šค ์ˆ˜์ค€์˜ ์„ฑ๋Šฅ์ด์ง€. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์‚ฌ์šฉ์ž๋“ค์ด ์—…๋กœ๋“œํ•œ ์ˆ˜๋งŽ์€ ํฌํŠธํด๋ฆฌ์˜ค ์ด๋ฏธ์ง€๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋ฐ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ!

๐ŸŽ“ ํŒŒ์ธํŠœ๋‹: ๋‚˜๋งŒ์˜ ๋„๋ฉ”์ธ์— ์ตœ์ ํ™”ํ•˜๊ธฐ

CLIP์€ ๋ฒ”์šฉ์ ์œผ๋กœ ํ›Œ๋ฅญํ•˜์ง€๋งŒ, ํŠน์ • ๋„๋ฉ”์ธ(์˜๋ฃŒ, ํŒจ์…˜, ๋ถ€๋™์‚ฐ ๋“ฑ)์—์„œ๋Š” ํŒŒ์ธํŠœ๋‹ํ•˜๋ฉด ํ›จ์”ฌ ์ข‹์€ ์„ฑ๋Šฅ์„ ๋‚ผ ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

๐Ÿ“š ํŒŒ์ธํŠœ๋‹ ์ค€๋น„

ํŒŒ์ธํŠœ๋‹ํ•˜๋ ค๋ฉด ๋„๋ฉ”์ธ ํŠนํ™” ๋ฐ์ดํ„ฐ์…‹์ด ํ•„์š”ํ•ด. ์ด๋ฏธ์ง€-ํ…์ŠคํŠธ ์Œ์œผ๋กœ ๊ตฌ์„ฑ๋œ ๋ฐ์ดํ„ฐ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŒจ์…˜ ๋„๋ฉ”์ธ์ด๋ผ๋ฉด:

ํŒจ์…˜ ๋ฐ์ดํ„ฐ์…‹ ์˜ˆ์‹œ - ์ด๋ฏธ์ง€: dress_001.jpg
- ํ…์ŠคํŠธ: "๋นจ๊ฐ„์ƒ‰ ํ”Œ๋ ˆ์–ด ์›ํ”ผ์Šค, ๋ฌด๋ฆŽ ๊ธธ์ด, ๋ฐ˜ํŒ”, ์—ฌ๋ฆ„์šฉ"

- ์ด๋ฏธ์ง€: shoes_045.jpg
- ํ…์ŠคํŠธ: "๊ฒ€์€์ƒ‰ ๊ฐ€์ฃฝ ์•ตํด๋ถ€์ธ , ์ฒญํ‚คํž, ์ง€ํผ ๋””ํ…Œ์ผ"

์ตœ์†Œ ์ˆ˜์ฒœ ๊ฐœ, ์ด์ƒ์ ์œผ๋กœ๋Š” ์ˆ˜๋งŒ ๊ฐœ์˜ ์Œ์ด ํ•„์š”ํ•ด!
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import clip

class CustomImageTextDataset(Dataset):
    def __init__(self, image_paths, captions, preprocess):
        self.image_paths = image_paths
        self.captions = captions
        self.preprocess = preprocess
    
    def __len__(self):
        return len(self.image_paths)
    
    def __getitem__(self, idx):
        image = self.preprocess(Image.open(self.image_paths[idx]))
        caption = self.captions[idx]
        return image, caption

def finetune_clip(model, train_loader, epochs=10, lr=1e-6, device="cuda"):
    """CLIP ํŒŒ์ธํŠœ๋‹"""
    # ์˜ตํ‹ฐ๋งˆ์ด์ € ์„ค์ • (ํ•™์Šต๋ฅ ์„ ๋งค์šฐ ๋‚ฎ๊ฒŒ!)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr, betas=(0.9, 0.98), eps=1e-6)
    
    # ํ•™์Šต๋ฅ  ์Šค์ผ€์ค„๋Ÿฌ
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
    
    model.train()
    
    for epoch in range(epochs):
        total_loss = 0
        
        for batch_idx, (images, texts) in enumerate(train_loader):
            images = images.to(device)
            texts = clip.tokenize(texts, truncate=True).to(device)
            
            # Forward pass
            logits_per_image, logits_per_text = model(images, texts)
            
            # Contrastive loss
            batch_size = images.shape[0]
            labels = torch.arange(batch_size).to(device)
            
            loss_i = nn.CrossEntropyLoss()(logits_per_image, labels)
            loss_t = nn.CrossEntropyLoss()(logits_per_text, labels)
            loss = (loss_i + loss_t) / 2
            
            # Backward pass
            optimizer.zero_grad()
            loss.backward()
            
            # ๊ทธ๋ž˜๋””์–ธํŠธ ํด๋ฆฌํ•‘ (์•ˆ์ •์„ฑ์„ ์œ„ํ•ด)
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            
            optimizer.step()
            
            total_loss += loss.item()
            
            if batch_idx % 100 == 0:
                print(f"Epoch {epoch+1}/{epochs}, Batch {batch_idx}, Loss: {loss.item():.4f}")
        
        scheduler.step()
        avg_loss = total_loss / len(train_loader)
        print(f"Epoch {epoch+1} ์™„๋ฃŒ, ํ‰๊ท  Loss: {avg_loss:.4f}")
        
        # ์ฒดํฌํฌ์ธํŠธ ์ €์žฅ
        torch.save({
            'epoch': epoch,
            'model_state_dict': model.state_dict(),
            'optimizer_state_dict': optimizer.state_dict(),
            'loss': avg_loss,
        }, f'clip_finetuned_epoch_{epoch+1}.pt')

# ์‚ฌ์šฉ ์˜ˆ์‹œ
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-B/32", device=device)

# ๋ฐ์ดํ„ฐ ์ค€๋น„
image_paths = ["path/to/image1.jpg", "path/to/image2.jpg", ...]
captions = ["caption for image 1", "caption for image 2", ...]

dataset = CustomImageTextDataset(image_paths, captions, preprocess)
train_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)

# ํŒŒ์ธํŠœ๋‹ ์‹œ์ž‘
finetune_clip(model, train_loader, epochs=10, lr=1e-6, device=device)
๐Ÿ’ก ํŒŒ์ธํŠœ๋‹ ํŒ 1. ํ•™์Šต๋ฅ ์„ ๋งค์šฐ ๋‚ฎ๊ฒŒ: 1e-6 ~ 1e-5 ์ •๋„. ๋„ˆ๋ฌด ๋†’์œผ๋ฉด ์‚ฌ์ „ํ•™์Šต๋œ ์ง€์‹์„ ์žƒ์–ด๋ฒ„๋ ค!
2. ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•: ์ด๋ฏธ์ง€ ํšŒ์ „, ํฌ๋กญ, ์ƒ‰์ƒ ์กฐ์ • ๋“ฑ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋Š˜๋ ค
3. ์ ์ง„์  ํ•™์Šต: ์ฒ˜์Œ์—” ๋งˆ์ง€๋ง‰ ๋ ˆ์ด์–ด๋งŒ, ๋‚˜์ค‘์— ์ „์ฒด ๋ชจ๋ธ์„ ํ•™์Šต
4. ๊ฒ€์ฆ ์„ธํŠธ: ๊ณผ์ ํ•ฉ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๊ฒ€์ฆ ์„ธํŠธ๋กœ ์„ฑ๋Šฅ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•ด!

๐ŸŒŸ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€: ํ˜„์—…์—์„œ๋Š” ์ด๋ ‡๊ฒŒ ์จ์š”

์ด๋ก ๊ณผ ์ฝ”๋“œ๋Š” ์ถฉ๋ถ„ํžˆ ๋ดค์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๐Ÿข

๐Ÿ›๏ธ ์ „์ž์ƒ๊ฑฐ๋ž˜: ๋น„์ฃผ์–ผ ๊ฒ€์ƒ‰

์˜จ๋ผ์ธ ์‡ผํ•‘๋ชฐ์—์„œ "์ด ์˜ท๊ณผ ๋น„์Šทํ•œ ์ƒํ’ˆ ์ฐพ๊ธฐ" ๊ธฐ๋Šฅ, ๋ณธ ์  ์žˆ์ง€? ๊ทธ๊ฒŒ ๋ฐ”๋กœ CLIP ๊ฐ™์€ ๊ธฐ์ˆ ์ด์•ผ! ์‚ฌ์šฉ์ž๊ฐ€ ๋งˆ์Œ์— ๋“œ๋Š” ์ƒํ’ˆ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๋ฉด, ๋น„์Šทํ•œ ์Šคํƒ€์ผ์˜ ์ƒํ’ˆ์„ ์ถ”์ฒœํ•ด์ฃผ๋Š” ๊ฑฐ์ง€. ๐Ÿ‘—

์‹ค์ œ ๊ตฌํ˜„ ํฌ์ธํŠธ:
- ์ „์ฒด ์ƒํ’ˆ ์นดํƒˆ๋กœ๊ทธ๋ฅผ ๋ฏธ๋ฆฌ ์ธ๋ฑ์‹ฑ
- ์‚ฌ์šฉ์ž ์—…๋กœ๋“œ ์ด๋ฏธ์ง€๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ธ์ฝ”๋”ฉ
- FAISS๋กœ ๋น ๋ฅธ ์œ ์‚ฌ ์ƒํ’ˆ ๊ฒ€์ƒ‰
- ๊ฐ€๊ฒฉ๋Œ€, ๋ธŒ๋žœ๋“œ ๋“ฑ ํ•„ํ„ฐ์™€ ๊ฒฐํ•ฉ

๐Ÿ“ฑ ์†Œ์…œ ๋ฏธ๋””์–ด: ์ฝ˜ํ…์ธ  ์ถ”์ฒœ

์ธ์Šคํƒ€๊ทธ๋žจ์ด๋‚˜ ํ•€ํ„ฐ๋ ˆ์ŠคํŠธ ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ "๋‹น์‹ ์ด ์ข‹์•„ํ•  ๋งŒํ•œ ๊ฒŒ์‹œ๋ฌผ"์„ ์ถ”์ฒœํ•  ๋•Œ๋„ ๋น„์Šทํ•œ ๊ธฐ์ˆ ์„ ์จ. ์‚ฌ์šฉ์ž๊ฐ€ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅธ ์ด๋ฏธ์ง€๋“ค์˜ ํŒจํ„ด์„ ํ•™์Šตํ•ด์„œ, ๋น„์Šทํ•œ ์Šคํƒ€์ผ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์ฐพ์•„์ฃผ๋Š” ๊ฑฐ์•ผ! ๐Ÿ“ธ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ์‚ฌ์šฉ์ž๊ฐ€ ๊ด€์‹ฌ ์žˆ์–ด ํ•˜๋Š” ํฌํŠธํด๋ฆฌ์˜ค ์Šคํƒ€์ผ์„ ๋ถ„์„ํ•ด์„œ, ๋น„์Šทํ•œ ์žฌ๋Šฅ์„ ๊ฐ€์ง„ ๋‹ค๋ฅธ ํฌ๋ฆฌ์—์ดํ„ฐ๋ฅผ ์ถ”์ฒœํ•ด์ฃผ๋Š” ๊ฑฐ์ง€. ๐ŸŽจ

๐Ÿฅ ์˜๋ฃŒ: ์˜๋ฃŒ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰

์˜์‚ฌ๋“ค์ด ๋น„์Šทํ•œ ์ฆ๋ก€๋ฅผ ์ฐพ์„ ๋•Œ๋„ CLIP์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด. "ํ๋ ด ํŒจํ„ด์ด ์žˆ๋Š” X-ray"์ฒ˜๋Ÿผ ์ž์—ฐ์–ด๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด, ๊ด€๋ จ ์˜๋ฃŒ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์•„์ฃผ๋Š” ๊ฑฐ์•ผ. ๋ฌผ๋ก  ์˜๋ฃŒ ๋„๋ฉ”์ธ์— ํŒŒ์ธํŠœ๋‹๋œ ๋ชจ๋ธ์„ ์จ์•ผ ํ•˜์ง€๋งŒ! ๐Ÿฅ

๐ŸŽฌ ๋ฏธ๋””์–ด: ๋น„๋””์˜ค ์ฝ˜ํ…์ธ  ๋ถ„์„

๋น„๋””์˜ค๋ฅผ ํ”„๋ ˆ์ž„๋ณ„๋กœ ๋ถ„์„ํ•ด์„œ ํŠน์ • ์žฅ๋ฉด์„ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด. "ํ•ด๋ณ€์—์„œ ์ผ๋ชฐ์„ ๋ณด๋Š” ์žฅ๋ฉด"์„ ๊ฒ€์ƒ‰ํ•˜๋ฉด, ์ˆ˜์‹œ๊ฐ„์งœ๋ฆฌ ์˜์ƒ์—์„œ ํ•ด๋‹น ์žฅ๋ฉด๋งŒ ์™ ์ฐพ์•„๋‚ด๋Š” ๊ฑฐ์ง€! ๐ŸŽฅ

import cv2

class VideoSearchEngine:
    def __init__(self):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model, self.preprocess = clip.load("ViT-B/32", device=self.device)
    
    def search_in_video(self, video_path, query, threshold=0.25, sample_rate=30):
        """๋น„๋””์˜ค์—์„œ ์ฟผ๋ฆฌ์™€ ๋งค์นญ๋˜๋Š” ์žฅ๋ฉด ์ฐพ๊ธฐ"""
        cap = cv2.VideoCapture(video_path)
        fps = cap.get(cv2.CAP_PROP_FPS)
        
        text = clip.tokenize([query]).to(self.device)
        with torch.no_grad():
            text_features = self.model.encode_text(text)
            text_features /= text_features.norm(dim=-1, keepdim=True)
        
        matching_frames = []
        frame_count = 0
        
        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
            
            # ์ƒ˜ํ”Œ๋ง (๋ชจ๋“  ํ”„๋ ˆ์ž„์„ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋„ˆ๋ฌด ๋А๋ฆผ)
            if frame_count % sample_rate == 0:
                # OpenCV BGR์„ RGB๋กœ ๋ณ€ํ™˜
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                pil_image = Image.fromarray(frame_rgb)
                
                image = self.preprocess(pil_image).unsqueeze(0).to(self.device)
                
                with torch.no_grad():
                    image_features = self.model.encode_image(image)
                    image_features /= image_features.norm(dim=-1, keepdim=True)
                    
                    similarity = (image_features @ text_features.T).item()
                
                if similarity > threshold:
                    timestamp = frame_count / fps
                    matching_frames.append({
                        'frame': frame_count,
                        'timestamp': timestamp,
                        'similarity': similarity
                    })
                    print(f"โœ… ๋งค์นญ ๋ฐœ๊ฒฌ: {timestamp:.2f}์ดˆ (์œ ์‚ฌ๋„: {similarity:.3f})")
            
            frame_count += 1
        
        cap.release()
        return matching_frames

# ์‚ฌ์šฉ ์˜ˆ์‹œ
video_search = VideoSearchEngine()
results = video_search.search_in_video(
    "movie.mp4",
    "a person running on the beach",
    threshold=0.3,
    sample_rate=15  # 15ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ƒ˜ํ”Œ๋ง
)

print(f"\n๐ŸŽฌ ์ด {len(results)}๊ฐœ ์žฅ๋ฉด ๋ฐœ๊ฒฌ!")

๐Ÿ”ฎ CLIP์˜ ๋ฏธ๋ž˜์™€ ๋ฐœ์ „ ๋ฐฉํ–ฅ

CLIP์€ ๊ณ„์† ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด! ์ตœ์‹  ํŠธ๋ Œ๋“œ์™€ ๋ฏธ๋ž˜ ๋ฐฉํ–ฅ์„ ์•Œ์•„๋ณด์ž. ๐Ÿš€

๐Ÿ†• CLIP์˜ ํ›„์† ๋ชจ๋ธ๋“ค

OpenCLIP ์˜คํ”ˆ์†Œ์Šค ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ๋งŒ๋“  CLIP ๊ตฌํ˜„์ฒด. ๋” ํฐ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ํ•™์Šต๋˜์–ด ์„ฑ๋Šฅ์ด ๋” ์ข‹์•„! ๐ŸŒŸ
BLIP / BLIP-2 Salesforce์—์„œ ๊ฐœ๋ฐœํ•œ ๋ชจ๋ธ๋กœ, CLIP๋ณด๋‹ค ์ด๋ฏธ์ง€-ํ…์ŠคํŠธ ์ดํ•ด๊ฐ€ ๋” ๋›ฐ์–ด๋‚˜. ํŠนํžˆ ์ด๋ฏธ์ง€ ์บก์…”๋‹์— ๊ฐ•ํ•ด! ๐Ÿ“
LLaVA, MiniGPT-4 CLIP๊ณผ ๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ(LLM)์„ ๊ฒฐํ•ฉํ•œ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ชจ๋ธ๋“ค. ์ด๋ฏธ์ง€๋ฅผ ๋ณด๊ณ  ๋Œ€ํ™”ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ฌ

๐ŸŽฏ ํ–ฅํ›„ ๋ฐœ์ „ ๋ฐฉํ–ฅ

1. ๋” ํฐ ๊ทœ๋ชจ
๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ, ๋” ํฐ ๋ชจ๋ธ๋กœ ์„ฑ๋Šฅ์ด ๊ณ„์† ํ–ฅ์ƒ๋˜๊ณ  ์žˆ์–ด. CLIP์˜ ํ•™์Šต ๋ฐ์ดํ„ฐ๋Š” 4์–ต ๊ฐœ์˜€์ง€๋งŒ, ์ตœ์‹  ๋ชจ๋ธ๋“ค์€ ์ˆ˜์‹ญ์–ต ๊ฐœ๋ฅผ ์‚ฌ์šฉํ•ด! ๐Ÿ“ˆ

2. ๋น„๋””์˜ค ์ดํ•ด
์ •์  ์ด๋ฏธ์ง€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋น„๋””์˜ค์˜ ์‹œ๊ฐ„์  ํ๋ฆ„๊นŒ์ง€ ์ดํ•ดํ•˜๋Š” ๋ชจ๋ธ๋“ค์ด ๋‚˜์˜ค๊ณ  ์žˆ์–ด. ๐ŸŽฌ

3. 3D ์ดํ•ด
2D ์ด๋ฏธ์ง€๋ฅผ ๋„˜์–ด 3D ๊ณต๊ฐ„์„ ์ดํ•ดํ•˜๋Š” ๋ชจ๋ธ๋“ค๋„ ์—ฐ๊ตฌ ์ค‘์ด์•ผ. AR/VR ๋ถ„์•ผ์—์„œ ํ™œ์šฉ๋  ๊ฑฐ์•ผ! ๐Ÿฅฝ

4. ํšจ์œจ์„ฑ ๊ฐœ์„ 
๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ์—์„œ๋„ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ๋Š” ๊ฒฝ๋Ÿ‰ํ™” ๋ชจ๋ธ๋“ค์ด ๊ฐœ๋ฐœ๋˜๊ณ  ์žˆ์–ด. ์—ฃ์ง€ ์ปดํ“จํŒ…์˜ ์‹œ๋Œ€! ๐Ÿ“ฑ

๐Ÿ’ก ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

์ด์ œ ๋ฐฐ์šด ๊ฑธ ํ™œ์šฉํ•ด์„œ ๋ญ˜ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„๊นŒ? ์žฌ๋ฏธ์žˆ๋Š” ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด๋“ค์„ ์†Œ๊ฐœํ• ๊ฒŒ! ๐ŸŽจ

๐ŸŽฏ ํ”„๋กœ์ ํŠธ 1: ์Šค๋งˆํŠธ ํฌํ†  ์•จ๋ฒ” "์ž‘๋…„ ์—ฌ๋ฆ„ ์ œ์ฃผ๋„ ์—ฌํ–‰ ์‚ฌ์ง„"์ฒ˜๋Ÿผ ์ž์—ฐ์–ด๋กœ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐœ์ธ ์‚ฌ์ง„ ๊ด€๋ฆฌ ์•ฑ. Google Photos์ฒ˜๋Ÿผ!

ํ•ต์‹ฌ ๊ธฐ๋Šฅ:
- ์ž๋™ ํƒœ๊ทธ ์ƒ์„ฑ
- ์ž์—ฐ์–ด ๊ฒ€์ƒ‰
- ๋น„์Šทํ•œ ์‚ฌ์ง„ ๊ทธ๋ฃนํ•‘
- ์ถ”์–ต ์ž๋™ ์ƒ์„ฑ
๐ŸŽฏ ํ”„๋กœ์ ํŠธ 2: ํŒจ์…˜ ์Šคํƒ€์ผ ์–ด์‹œ์Šคํ„ดํŠธ ์˜ท์žฅ ์‚ฌ์ง„์„ ์ฐ์œผ๋ฉด ์ž๋™์œผ๋กœ ์ฝ”๋””๋ฅผ ์ถ”์ฒœํ•ด์ฃผ๋Š” ์•ฑ!

ํ•ต์‹ฌ ๊ธฐ๋Šฅ:
- ์˜๋ฅ˜ ์ž๋™ ๋ถ„๋ฅ˜ (์ƒ์˜, ํ•˜์˜, ์‹ ๋ฐœ ๋“ฑ)
- ์ƒ‰์ƒ/์Šคํƒ€์ผ ๋ถ„์„
- ๋‚ ์”จ/์ƒํ™ฉ๋ณ„ ์ฝ”๋”” ์ถ”์ฒœ
- ๋น„์Šทํ•œ ์Šคํƒ€์ผ ์‡ผํ•‘ ์ถ”์ฒœ
๐ŸŽฏ ํ”„๋กœ์ ํŠธ 3: ์ธํ…Œ๋ฆฌ์–ด ์•„์ด๋””์–ด ๊ฒ€์ƒ‰ "๋ถ์œ ๋Ÿฝ ์Šคํƒ€์ผ ๊ฑฐ์‹ค"์ฒ˜๋Ÿผ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๊ด€๋ จ ์ธํ…Œ๋ฆฌ์–ด ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์•„์ฃผ๋Š” ์„œ๋น„์Šค!

ํ•ต์‹ฌ ๊ธฐ๋Šฅ:
- ์Šคํƒ€์ผ๋ณ„ ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰
- ๋น„์Šทํ•œ ๊ฐ€๊ตฌ ์ฐพ๊ธฐ
- ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ ์ถ”์ถœ
- ์‡ผํ•‘๋ชฐ ์—ฐ๋™
๐ŸŽฏ ํ”„๋กœ์ ํŠธ 4: ํ•™์Šต ์ž๋ฃŒ ์ •๋ฆฌ ๋„๊ตฌ ์Šคํฌ๋ฆฐ์ƒท์ด๋‚˜ ์‚ฌ์ง„์œผ๋กœ ์ฐ์€ ํ•™์Šต ์ž๋ฃŒ๋ฅผ ์ž๋™์œผ๋กœ ๋ถ„๋ฅ˜ํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ!

ํ•ต์‹ฌ ๊ธฐ๋Šฅ:
- ๊ณผ๋ชฉ/์ฃผ์ œ๋ณ„ ์ž๋™ ๋ถ„๋ฅ˜
- ๋‚ด์šฉ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰
- ๊ด€๋ จ ์ž๋ฃŒ ์ถ”์ฒœ
- ๋ณต์Šต ์Šค์ผ€์ค„ ์ƒ์„ฑ

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ

๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด? ์œ ์šฉํ•œ ๋ฆฌ์†Œ์Šค๋“ค์„ ๋ชจ์•„๋ดค์–ด! ๐Ÿ“š

๐Ÿ“– ๊ณต์‹ ๋ฌธ์„œ ๋ฐ ๋…ผ๋ฌธ

ํ•„์ˆ˜ ์ฝ์„๊ฑฐ๋ฆฌ 1. CLIP ์›๋ณธ ๋…ผ๋ฌธ: "Learning Transferable Visual Models From Natural Language Supervision"
2. OpenAI CLIP GitHub: ๊ณต์‹ ์ฝ”๋“œ์™€ ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ
3. Hugging Face Transformers ๋ฌธ์„œ: CLIP ์‚ฌ์šฉ ๊ฐ€์ด๋“œ
4. OpenCLIP: ์ปค๋ฎค๋‹ˆํ‹ฐ ๋ฒ„์ „ CLIP

๐Ÿ’ป ์‹ค์Šต ํ”Œ๋žซํผ

Google Colab: ๋ฌด๋ฃŒ GPU๋กœ CLIP ์‹ค์Šตํ•˜๊ธฐ ์ข‹์•„! ๐Ÿ†“
Kaggle Notebooks: ๋‹ค์–‘ํ•œ CLIP ํ™œ์šฉ ์˜ˆ์ œ๋“ค
Hugging Face Spaces: ์›น์—์„œ ๋ฐ”๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ๋ชจ๋“ค

๐ŸŒ ์ปค๋ฎค๋‹ˆํ‹ฐ

Reddit r/MachineLearning: ์ตœ์‹  ์—ฐ๊ตฌ ๋™ํ–ฅ
Papers with Code: CLIP ๊ด€๋ จ ๋…ผ๋ฌธ๊ณผ ์ฝ”๋“œ
Discord AI ์ปค๋ฎค๋‹ˆํ‹ฐ๋“ค: ์‹ค์‹œ๊ฐ„ ์งˆ๋ฌธ๊ณผ ๋‹ต๋ณ€
์žฌ๋Šฅ๋„ท AI/๋จธ์‹ ๋Ÿฌ๋‹ ์นดํ…Œ๊ณ ๋ฆฌ: ํ•œ๊ตญ์–ด๋กœ ์†Œํ†ตํ•˜๋ฉฐ ๋ฐฐ์šฐ๊ธฐ! ๐Ÿ‡ฐ๐Ÿ‡ท

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐ŸŽ‰ ์šฐ๋ฆฌ๋Š” CLIP์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ, ์‹ค์ „ ๊ตฌํ˜„, ์ตœ์ ํ™”, ๊ณ ๊ธ‰ ํ™œ์šฉ๊นŒ์ง€ ๋ชจ๋“  ๊ฑธ ๋‹ค๋ค˜์–ด!

CLIP์€ ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ. ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑด, ๋ฌด๊ถ๋ฌด์ง„ํ•œ ๊ฐ€๋Šฅ์„ฑ์„ ์˜๋ฏธํ•ด. ๊ฒ€์ƒ‰ ์—”์ง„, ์ถ”์ฒœ ์‹œ์Šคํ…œ, ์ฝ˜ํ…์ธ  ๋ชจ๋”๋ ˆ์ด์…˜, ํฌ๋ฆฌ์—์ดํ‹ฐ๋ธŒ ํˆด... ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ๋ถ„์•ผ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์ง€! ๐ŸŒˆ

์ค‘์š”ํ•œ ๊ฑด ์ง์ ‘ ํ•ด๋ณด๋Š” ๊ฑฐ์•ผ! ์ด ๊ธ€์—์„œ ์†Œ๊ฐœํ•œ ์ฝ”๋“œ๋“ค์„ ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ๋งŒ ํ•˜์ง€ ๋ง๊ณ , ์ง์ ‘ ์ˆ˜์ •ํ•ด๋ณด๊ณ  ์‹คํ—˜ํ•ด๋ด. ์‹คํŒจ๋„ ํ•ด๋ณด๊ณ , ์—๋Ÿฌ๋„ ๋งŒ๋‚˜๋ณด๊ณ , ๊ทธ๋Ÿฌ๋ฉด์„œ ์ง„์งœ ์‹ค๋ ฅ์ด ๋Š˜์–ด๋‚˜๋Š” ๊ฑฐ์•ผ. ๐Ÿ’ช

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์—ฌ๋Ÿฌ๋ถ„์˜ CLIP ํ”„๋กœ์ ํŠธ๋ฅผ ๊ณต์œ ํ•˜๊ณ , ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค๊ณผ ํ˜‘์—…ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํ˜ผ์ž ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ•จ๊ป˜ ํ•˜๋ฉด ํ›จ์”ฌ ๋นจ๋ฆฌ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿค

AI ๊ธฐ์ˆ ์€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. CLIP๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์ด ๋‚ด์ผ์ด๋ฉด ๊ตฌ์‹์ด ๋  ์ˆ˜๋„ ์žˆ์ง€. ํ•˜์ง€๋งŒ ๊ธฐ๋ณธ ์›๋ฆฌ์™€ ๊ฐœ๋…์€ ๋ณ€ํ•˜์ง€ ์•Š์•„. ๊ทธ๊ฑธ ํƒ„ํƒ„ํžˆ ์ดํ•ดํ•˜๊ณ  ์žˆ์œผ๋ฉด, ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์ด ๋‚˜์™€๋„ ๊ธˆ๋ฐฉ ๋”ฐ๋ผ์žก์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

์ž, ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„ ์ฐจ๋ก€์•ผ! ํ‚ค๋ณด๋“œ๋ฅผ ๋‘๋“œ๋ฆฌ๊ณ , ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ๋ฉ‹์ง„ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ด. ๊ทธ๋ฆฌ๊ณ  ๋ง‰ํžˆ๋Š” ๊ฒŒ ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๋ฌผ์–ด๋ด. ์šฐ๋ฆฌ ๋ชจ๋‘ ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ฑฐ๋‹ˆ๊นŒ! ๐Ÿ˜Š

ํ–‰์šด์„ ๋นŒ์–ด! ๊ทธ๋ฆฌ๊ณ  ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉ ๋˜๊ธธ! ๐ŸŽจโœจ

Happy Coding! CLIP์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฉ‹์ง„ ์„ธ์ƒ ๐Ÿš€ ๐ŸŽจ ๐Ÿ’ก ๐ŸŒŸ
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.