Files
Source/ollama/lanchain-vision.ipynb
cooney ccfdac1286 1. 랭체인 이미지 인식 후 처리 마무리
2. fastAPI로 프로젝트 구조 실습

1. 랭체인 이미지 인식 후 처리 마무리
2. fastAPI로 프로젝트 구조 실습
2026-06-15 18:13:05 +09:00

835 lines
32 KiB
Plaintext

{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### Vision 모델 내부 동작 원리\n",
"- 이미지 인코딩 : 이미지 임베딩 벡터\n",
"- 프로젝션 : 이미지 벡터를 LLM 토큰 공간으로 변환\n",
"- 토큰 결합 : 시각 토큰 + 텍스트 토큰을 순서대로 배열\n",
"- LLM 처리\n",
"- 텍스트 생성"
],
"id": "3ed294a9c2fd2cf5"
},
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-06-15T03:06:07.002963942Z",
"start_time": "2026-06-15T03:06:05.360626592Z"
}
},
"source": [
"import base64\n",
"\n",
"from jedi.inference import analysis\n",
"from langchain_core import documents\n",
"from langchain_ibm import WatsonxEmbeddings\n",
"from langchain_ibm import ChatWatsonx\n",
"\n",
"from dotenv import load_dotenv\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"from langchain_chroma import Chroma\n",
"import os\n",
"\n",
"from langchain_ollama import ChatOllama\n",
"from pathlib import Path\n",
"\n",
"from langchain_core.documents import Document\n",
"from langchain_core.messages import HumanMessage"
],
"outputs": [],
"execution_count": 3
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:07.012203786Z",
"start_time": "2026-06-15T03:06:07.003754555Z"
}
},
"cell_type": "code",
"source": [
"# .env 내용 가져오기\n",
"load_dotenv()\n",
"\n",
"apikey = os.getenv(\"WATSONX_API_KEY\")\n",
"project_id = os.getenv(\"WATSONX_PROJECT_ID\")\n",
"watsonx_ai_url = os.getenv(\"WATSONX_URL\")"
],
"id": "27bd39a0da002140",
"outputs": [],
"execution_count": 4
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:09.243124960Z",
"start_time": "2026-06-15T03:06:07.013041549Z"
}
},
"cell_type": "code",
"source": [
"watson_llm = ChatWatsonx(\n",
" model_id=\"ibm/granite-4-h-small\",\n",
" url=f\"{watsonx_ai_url}\",\n",
" api_key=f\"{apikey}\",\n",
" project_id=f\"{project_id}\",\n",
" max_tokens=2000,\n",
" params={\n",
" \"temperature\": 0\n",
" }\n",
")\n",
"\n",
"watson_embedding = WatsonxEmbeddings(\n",
" model_id=\"ibm/granite-embedding-278m-multilingual\",\n",
" url=f\"{watsonx_ai_url}\",\n",
" api_key=f\"{apikey}\",\n",
" project_id=f\"{project_id}\"\n",
")\n",
"\n",
"# 로컬 LLM\n",
"qwen_llm = ChatOllama(model=\"qwen3.5:4b\", temperature=0)\n",
"exaone_llm = ChatOllama(model=\"exaone3.5:2.4b\", temperature=0)\n",
"gemma_llm = ChatOllama(model=\"gemma4:e2b\")"
],
"id": "1b23141c1b60154a",
"outputs": [],
"execution_count": 5
},
{
"metadata": {},
"cell_type": "markdown",
"source": "#### 1. 로컬 이미지",
"id": "ef95d69bf02f830b"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:09.262869259Z",
"start_time": "2026-06-15T03:06:09.252393224Z"
}
},
"cell_type": "code",
"source": [
"# images를 base64 인코딩\n",
"\n",
"def encode_image(image_path:str)->str:\n",
" with open(image_path, \"rb\") as f:\n",
" return base64.b64encode(f.read()).decode(\"utf-8\")"
],
"id": "637e09cdf9c78eee",
"outputs": [],
"execution_count": 6
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:09.329919223Z",
"start_time": "2026-06-15T03:06:09.264035014Z"
}
},
"cell_type": "code",
"source": [
"# vision 모델\n",
"vision_llm = ChatOllama(model=\"minimax-m3:cloud\", temperature=0)\n",
"\n",
"parser = StrOutputParser()\n",
"\n",
"# 이미지 + 텍스트 : 이미지 기반으로 질문\n",
"def ask_about_image(image_path, question):\n",
" image_b64 = encode_image(image_path)\n",
" prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"human\",\n",
" [\n",
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{image_b64}'}},\n",
" {\"type\":\"text\", \"text\":question}\n",
" ]\n",
" )\n",
" ]\n",
" )\n",
"\n",
" chain = prompt | vision_llm\n",
" response = chain.invoke({\"image_b64\":image_b64, \"question\":question})\n",
" return response"
],
"id": "a1114bd96f354025",
"outputs": [],
"execution_count": 7
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:21.590918755Z",
"start_time": "2026-06-15T03:06:09.330837660Z"
}
},
"cell_type": "code",
"source": "print(ask_about_image(\"./image/animals1.jpg\", \"이 이미지에서 무엇이 보이나요?\"))",
"id": "25f2f6918598e8e0",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"content='이 이미지에서는 카메라를 올려다보는 고양이가 보입니다. 주요 특징은 다음과 같습니다:\\n\\n- **품종**: 얼룩무늬 고양이(태비)로, 회색, 검은색, 갈색이 섞인 줄무늬 패턴을 가지고 있습니다.\\n- **눈**: 크고 둥근 밝은 청록색/연한 녹색 눈동자로, 위쪽에서 비스듬히 내려다보는 각도로 촬영되었습니다.\\n- **표정**: 입이 살짝 벌어져 있어 마치 울거나 야옹하는 것처럼 보이며, 작은 이빨이 살짝 보입니다.\\n- **수염**: 길고 곧은 흰색 수염이 양쪽으로 뻗어 있습니다.\\n- **배경**: 따뜻한 톤의 나무 바닥이나 가구 위에 있는 것으로 보입니다.\\n\\n전체적으로 아기자기하면서도 약간 놀라 보이거나 무언가를 요구하는 듯한 표정의 고양이 클로즈업 사진입니다.' additional_kwargs={} response_metadata={'model': 'minimax-m3', 'created_at': '2026-06-15T03:06:21.490583355Z', 'done': True, 'done_reason': 'stop', 'total_duration': 11626185191, 'load_duration': None, 'prompt_eval_count': 511, 'prompt_eval_duration': None, 'eval_count': 231, 'eval_duration': None, 'logprobs': None, 'model_name': 'minimax-m3', 'model_provider': 'ollama'} id='lc_run--019ec93e-624f-7b11-89b0-cd5aabe517b3-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 511, 'output_tokens': 231, 'total_tokens': 742}\n"
]
}
],
"execution_count": 8
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:21.612768204Z",
"start_time": "2026-06-15T03:06:21.600081143Z"
}
},
"cell_type": "code",
"source": "#### 2. URL 이미지",
"id": "79877a4f0166e70d",
"outputs": [],
"execution_count": 9
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:57.034661130Z",
"start_time": "2026-06-15T03:06:21.613674680Z"
}
},
"cell_type": "code",
"source": [
"import requests\n",
"from io import BytesIO\n",
"from PIL import Image\n",
"\n",
"# 이미지 다운로드\n",
"url ='https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQK4_aoipvzj-4LsUx-FAtPmRnkgtOOMbRAQOfBceT5UQ&s=10'\n",
"img = requests.get(url).content\n",
"# 인코딩\n",
"img_b64 = base64.b64encode(img).decode(\"utf-8\")\n",
"\n",
"# 모델\n",
"message = HumanMessage(\n",
" content = [\n",
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
" {\"type\":\"text\", \"text\": \"이 동물의 종류와 특징을 설명해주세요.\"}\n",
" ]\n",
" )\n",
"\n",
"vision_llm.invoke([message]).content"
],
"id": "bcc767a8ef31098b",
"outputs": [
{
"data": {
"text/plain": [
"'# 🐱 고양이 품종 분석\\n\\n이미지에 보이는 고양이는 **장모종(Long-haired breed)** 의 새끼 고양이로 보입니다.\\n\\n## 추정 품종\\n\\n### 1. 시베리안 고양이 (Siberian Cat)\\n가장 가능성이 높은 품종입니다.\\n- **러시아 원산**의 자연산 출생 품종\\n- 풍성한 이중 모피(Double coat)\\n- 추운 기후에 적응한 풍성한 꼬리(브러시 테일)\\n\\n### 2. 노르웨이 숲고양이 (Norwegian Forest Cat)\\n가능성이 있는 대체 품종\\n- **북유럽(노르웨이) 원산**\\n- 비슷한 외형의 대형 장모종\\n\\n### 3. 메인쿤 (Maine Coon)\\n가능성 있음\\n- 미국 원산의 대형 품종\\n- 귀 끝에 술(lynx tips)이 있는 것이 특징\\n\\n## 외형적 특징\\n\\n| 특징 | 설명 |\\n|------|------|\\n| **털 색깔** | 그레이 & 화이트 이색(Bicolor) |\\n| **모피** | 길고 부드러운 장모, 이중 코트 |\\n| **귀** | 크고 뾰족하며 끝에 작은 술이 있음 |\\n| **눈** | 크고 둥글며 밝은 색(황록색) |\\n| **체형** | 통통하고 근육질, 강건한 골격 |\\n| **꼬리** | 매우 풍성한 브러시 형태 |\\n\\n## 성격적 특징 (장모종 공통)\\n\\n- 🌟 **온화하고 친근한 성격**\\n- 🧠 **높은 지능과 호기심**\\n- 💧 **물을 좋아하는 편** (대부분의 고양이와 다름)\\n- 👨\\u200d👩\\u200d👧 **가족에게 애착이 강함**\\n- 🐾 **활동적이고 장난기 많음**\\n\\n## 관리 포인트\\n\\n- **정기적인 빗질**: 매일 1회, 털 엉킴 방지\\n- **철저한 위생 관리**: 장모는 배변 후 엉덩이 털 관리가 필요\\n- **운동 공간 확보**: 대형 품종으로 충분히 뛰어놀 수 있는 공간 필요\\n- **식단 관리**: 풍성한 모피 유지를 위한 고단백질 사료\\n\\n이처럼 사랑스럽고 아름다운 새끼 고양이는 성격이 매우 좋고, 가족과 함께하면 훌륭한 반려동물이 될 것입니다! 💕'"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 10
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:57.059273077Z",
"start_time": "2026-06-15T03:06:57.045190160Z"
}
},
"cell_type": "code",
"source": "#### 3. OCR",
"id": "9549854d2a4d9c89",
"outputs": [],
"execution_count": 11
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:06:57.080602805Z",
"start_time": "2026-06-15T03:06:57.060282470Z"
}
},
"cell_type": "code",
"source": [
"# 모든 이미지를 jpeg 저장 / 인코딩\n",
"\n",
"def pil_to_base64(img:Image.Image,format=\"JPEG\")->str:\n",
" buffer = BytesIO()\n",
" img.save(buffer, format=format)\n",
"\n",
" return base64.b64encode(buffer.getvalue()).decode(\"utf-8\")"
],
"id": "32f3f6d75760fcec",
"outputs": [],
"execution_count": 12
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:07:15.435586589Z",
"start_time": "2026-06-15T03:06:57.083111604Z"
}
},
"cell_type": "code",
"source": [
"img = Image.open(\"./image/system.png\")\n",
"\n",
"# 리사이즈\n",
"img_resized = img.resize((800,600))\n",
"\n",
"# Gray\n",
"img_gray = img_resized.convert(\"L\")\n",
"img_b64 = pil_to_base64(img_gray)\n",
"\n",
"message = HumanMessage(\n",
" content = [\n",
" {\"type\":\"text\", \"text\":\"이 문서의 이미지에서 텍스트를 추출해주세요\"},\n",
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
"\n",
" ]\n",
" )\n",
"\n",
"result = vision_llm.invoke([message]).content\n",
"print(result)"
],
"id": "18e51689e189291f",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# 문서 텍스트 추출\n",
"\n",
"## 미래로봇추진단(서울 근무)\n",
"### S/W개발 - 시스템/소프트웨어\n",
"\n",
"---\n",
"\n",
"**포지션 소개 (Job Overview)**\n",
"휴머노이드 로봇의 실시간 제어 시스템 및 소프트웨어 플랫폼을 개발하는 직무입니다. 운영체제 환경 구성, 디바이스 드라이버, 실시간 제어 프레임워크 등 로봇 동작의 핵심 기반이 되는 소프트웨어를 설계 및 개발하며, 하드웨어 설계 조직 및 AI 연구 조직과 긴밀히 협업합니다.\n",
"\n",
"---\n",
"\n",
"**수행업무 (Job Details)**\n",
"- 휴머노이드 로봇의 실시간 제어 프레임워크를 설계하고 개발합니다.\n",
"- 로보, 센서 등 로봇 하드웨어 제어를 위한 디바이스 드라이버 및 하드웨어 추상화 계층을 개발합니다.\n",
"- 로봇 제어를 운영체제(Linux 기반 실시간 OS 등) 환경에 구성하고 시스템 성능을 최적화합니다.\n",
"- 유관 부서와 협업하여 로봇 조직 등 시 기능과 제어 시스템 간 연동 미들웨어를 개발합니다.\n",
"\n",
"---\n",
"\n",
"**자격요건 (Requirements)**\n",
"- 컴퓨터, 전기·전자, 기계, 로봇공학 등 관련 전공을 하신 분\n",
"- 운영체제 기반 개념에 대한 이해도를 보유하신 분\n",
"- 요구사항을 분석하여 소프트웨어를 구조적으로 설계 및 구현하는 역량을 보유하신 분\n",
"- 다양한 분야의 엔지니어와 적극적으로 소통하며 협업할 수 있는 역량을 보유하신 분\n",
"\n",
"---\n",
"\n",
"**우대사항 (Preferences)**\n",
"- C/C++ 기반 시스템 프로그래밍 역량을 보유하신 분\n",
"- Linux 기반 임베디드 시스템 개발 경험을 보유하신 분 (Kernel, Device Driver, BSP 등)\n",
"- CAN, EtherCAT, UDP 등 하드웨어 통신 프로토콜 활용 경험을 보유하신 분\n",
"- ROS2 기반 로봇제어 소프트웨어 수행 경험을 보유하신 분\n",
"- Git 기반 협업 및 CI/CD 환경에서의 개발 경험을 보유하신 분\n",
"\n",
"---\n",
"\n",
"**커리어 비전 (Career Vision)**\n",
"휴머노이드 로봇의 초기 개발 단계부터 참여하여 핵심 개발자로 성장할 수 있습니다. 실시간 제어, 시스템 아키텍처, 로봇 미들웨어 등 다양한 경험을 통해 로봇틱스 시스템 SW 전문가, 나아가 시스템 아키텍트로의 커리어 확장이 가능합니다.\n",
"\n",
"---\n",
"\n",
"> **\"휴머노이드의 미래를 함께 설계할 분을 찾습니다\"**\n",
"> \n",
"> 로봇이 인정적으로 동작하기 위한 소프트웨어 기반을 함께 만들어가자. 도전적인 기술 과제를 즐기는 분의 지원을 환영합니다.\n"
]
}
],
"execution_count": 13
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:07:15.454781421Z",
"start_time": "2026-06-15T03:07:15.444138572Z"
}
},
"cell_type": "code",
"source": [
"# 다중 이미지 비교분석\n",
"\n",
"def compare_images(image_paths, question):\n",
" content = []\n",
"\n",
" for i, path in enumerate(image_paths, 1):\n",
" img_b64 = encode_image(path)\n",
" content.append({\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}})\n",
" content.append({\"type\":\"text\", \"text\": f\"[이미지 {i}]\"})\n",
"\n",
" # 질문 추가\n",
" content.append({\"type\":\"text\", \"text\":question})\n",
" message = HumanMessage(content=content)\n",
" response = vision_llm.invoke([message])\n",
" return response.content"
],
"id": "d184430da51ccc36",
"outputs": [],
"execution_count": 14
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:07:32.850368373Z",
"start_time": "2026-06-15T03:07:15.455557487Z"
}
},
"cell_type": "code",
"source": [
"result = compare_images(['./image/fridge.jpg', './image/table.jpg'], \"두 제품의 차이점을 비교 분석해 주세요.\")\n",
"print(result)"
],
"id": "baa2ba7a8d4a6b10",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# 두 제품 비교 분석\n",
"\n",
"## 🖼️ 이미지 1: 냉장고 (4도어 프렌치 도어형)\n",
"\n",
"### 주요 특징\n",
"- **구조**: 4도어 프렌치 도어 디자인 (상단 2개 + 하단 2개)\n",
"- **색상**: 화이트 / 크림색\n",
"- **디자인**: 미니멀하고 깔끔한 라인\n",
"- **디스플레이**: 중앙에 검은색 디지털 디스플레이 패널\n",
"- **바퀴**: 하단 조절 가능한 발\n",
"\n",
"### 용도\n",
"주방용 대형 가전제품 (식품 보관)\n",
"\n",
"---\n",
"\n",
"## 🖼️ 이미지 2: 식탁 / 다이닝 테이블\n",
"\n",
"### 주요 특징\n",
"- **소재**: 대리석 (화이트 + 그레이 veins) 상판\n",
"- **다리**: 검은색/다크 컬러 + 골드(Gold) 팁\n",
"- **형태**: 직사각형, 라운드 코너 처리\n",
"- **구조**: 모던한 4각 다리형\n",
"- **수납**: 하단 선반 구조\n",
"\n",
"### 용도\n",
"거실/식당/다이닝 공간 가구\n",
"\n",
"---\n",
"\n",
"## 📊 핵심 차이점 비교\n",
"\n",
"| 구분 | 냉장고 | 식탁 |\n",
"|------|--------|------|\n",
"| **카테고리** | 가전제품 | 가구 |\n",
"| **소재** | 금속/플라스틱 | 대리석 + 금속 |\n",
"| **용도** | 식품 보관·냉각 | 식사·작업 공간 |\n",
"| **크기** | 대형 (약 180cm+) | 중형 (140~180cm) |\n",
"| **가격대** | 고가 (100만원~) | 중~고가 |\n",
"| **색감** | 모노톤 화이트 | 화이트+블랙+골드 |\n",
"| **디자인 스타일** | 미니멀 모던 | 럭셔리 모던 |\n",
"\n",
"---\n",
"\n",
"## 🎨 공통점\n",
"- **모던한 디자인**: 두 제품 모두 현대적인 인테리어에 어울리는 깔끔한 스타일\n",
"- **화이트 톤**: 밝고 깨끗한 인상을 주는 화이트 계열\n",
"- **고급스러움**: 프리미엄 제품군의 디자인 철학\n",
"\n",
"## 💡 인테리어 활용\n",
"두 제품 모두 **모던 미니멀** 또는 **럭셔리 모던** 스타일 주방에 잘 어울리며, 화이트 톤의 통일성으로 공간의 시너지를 극대화할 수 있습니다.\n"
]
}
],
"execution_count": 15
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:08:00.503202012Z",
"start_time": "2026-06-15T03:07:32.869952272Z"
}
},
"cell_type": "code",
"source": [
"result = compare_images(['./image/chart1.png', './image/chart2.png'], \"두 제품의 차트를을 비교하여 주요 변화 추이를 설명해주세요.\")\n",
"print(result)"
],
"id": "891a618c4e36fea9",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# 두 차트 비교 분석\n",
"\n",
"## 📊 차트 1: 최근 6개월 전체 화장품 수출 추이\n",
"\n",
"| 기간 | 수출액(백만$) | YoY 증감률 |\n",
"|------|---------------|------------|\n",
"| 25년 12월 | 883.7 | +10.2% |\n",
"| 26년 1월 | 841.6 | **+33.7%** (최고) |\n",
"| 26년 2월 | 752.1 | +1.7% (최저) |\n",
"| 26년 3월 | 960.4 | +23.0% |\n",
"| 26년 4월 | **1,096.3** (최고 수출액) | +21.7% |\n",
"| 26년 5월(1~20일) | 671.1 | **-16.0%** (전월 대비 급락) |\n",
"\n",
"## 📊 차트 2: 5월 1~20일 주요 국가별 일평균 수출액\n",
"\n",
"| 국가 | 일평균 수출액(백만$) | YoY 증감률 |\n",
"|------|---------------------|------------|\n",
"| 중화권(중국) | 13.13 | **+76.4%** (최고) |\n",
"| 미국 | 11.66 | +40.3% |\n",
"| 유럽 | 11.14 | +61.3% |\n",
"| 동남아 | 4.91 | +22.0% |\n",
"| 일본 | 3.xx | **-14.1%** (유일한 마이너스) |\n",
"\n",
"---\n",
"\n",
"## 🔍 주요 변화 추이 분석\n",
"\n",
"### 1️⃣ **전체 수출의 5월 급격한 반전**\n",
"- 1~4월은 두 자릿수 성장(YoY +20~33%)을 유지하며 **우상향 흐름**이었으나, 5월(1~20일) 들어 **-16% 마이너스 성장**으로 전환\n",
"- 4월 대비 약 425백만$ 감소하며 **6개월 중 최저 수출액** 기록\n",
"\n",
"### 2️⃣ **국가별 양극화 현상**\n",
"- ✅ **호조**: 중화권(76.4%), 유럽(61.3%), 미국(40.3%) 등 신흥·대형 시장은 여전히 고성장 지속\n",
"- ❌ **악화**: 일본은 유일한 마이너스(-14.1%) 국가로, **일·중 갈등 리스크**(수출 규제 등)가 직접적 영향으로 작용한 것으로 추정\n",
"\n",
"### 3️⃣ **시장 다변화 효과**\n",
"- 5월 전체 수출 급감에도 **중국·유럽·미국** 비중이 절대적(1일 평균 11백만$ 이상)이라, 일본 위축이 전체 실적을 끌어내린 구조\n",
"- 중화권 일평균 수출액(13.13M$)이 전체 5월 실적의 약 **39%**(671.1M$ ÷ 20일 = 33.6M$ 기준)를 차지\n",
"\n",
"### 4️⃣ **핵심 인사이트**\n",
"> 📌 **\"성장 동력은 견고하나, 지정학적 리스크 노출도 확대\"**\n",
"> - 美·유럽·중국의 K-뷰티 수요는 여전히 강력\n",
"> - 그러나 **일본 1개국의 감소**가 전체 수치를 마이너스로 전환시킬 정도로 **일본 시장 의존도 리스크**가 부각\n",
"> - 향후 **대일 수출 정상화 여부**가 6월 이후 실적 반등의 핵심 변수가 될 전망\n",
"\n",
"---\n",
"\n",
"**요약**: 두 차트를 종합하면, 2026년 상반기 화장품 수출은 **1~4월 고성장 → 5월 급락**의 V자 변동을 보였으며, 이는 **신흥 시장 확대(긍정)**와 **일본 시장 위축(부정)**의 상반된 요인이 결합된 결과로 해석됩니다.\n"
]
}
],
"execution_count": 16
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"### 멀티모달\n",
" - 이미지 기반 문서 분석 시스템"
],
"id": "9687a7578057efa4"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:32.239301275Z",
"start_time": "2026-06-15T03:32:32.212206906Z"
}
},
"cell_type": "code",
"source": [
"# 이미지 => 설명문 생성\n",
"\n",
"def image_to_caption(image_path):\n",
" img_b64 = encode_image(image_path)\n",
" message = HumanMessage(\n",
" content = [\n",
" {\"type\":\"image_url\", \"image_url\":{'url':f'data:image/jpeg;base64,{img_b64}'}},\n",
" {\"type\":\"text\", \"text\": \"\"\"\n",
" 이 이미지를 검색용 설명문으로 요약하세요.\n",
" 200자 이내로 작성하세요.\n",
" 핵심 객체와 텍스트만 포함하세요.\n",
"\"\"\"}\n",
" ]\n",
" )\n",
" return vision_llm.invoke([message]).content"
],
"id": "972db1f4ffac7c6f",
"outputs": [],
"execution_count": 32
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:43.666106534Z",
"start_time": "2026-06-15T03:32:32.758731810Z"
}
},
"cell_type": "code",
"source": [
"caption = image_to_caption('./image/chart1.png')\n",
"caption"
],
"id": "5b60e8a76d1a4d47",
"outputs": [
{
"data": {
"text/plain": [
"'최근 6개월 화장품 수출액 및 전년동기 대비(YoY) 증감률 추이를 보여주는 막대·선 혼합 차트. 25년 12월부터 26년 5월(1~20일)까지 수출액은 752.1~1096.3백만 달러 범위, YoY는 33.7%에서 -16%까지 변동. 4월 수출액 1096.3백만 달러로 최고, 5월 -16%로 최저 성장률 기록.'"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 33
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:43.681737845Z",
"start_time": "2026-06-15T03:32:43.668688923Z"
}
},
"cell_type": "code",
"source": [
"# Document\n",
"\n",
"def build_multimodal_index(image_dir:str, text_docs:list[Document]):\n",
" all_docs = list(text_docs)\n",
"\n",
" # image_dir 안 파일 가져오기\n",
" image_files = list(Path(image_dir).glob(\"*.{jpg, jpeg, png}\"))\n",
"\n",
" # 캡션 생성 => Document => 임베딩\n",
" for img_path in image_files:\n",
" caption = image_to_caption(image_path = img_path)\n",
"\n",
" doc = Document(page_content=caption, metadata={\n",
" \"source\" : str(img_path),\n",
" \"type\" : \"image\",\n",
" \"image_path\" : str(img_path)\n",
" })\n",
" all_docs.append(doc)\n",
"\n",
" Chroma.from_documents(all_docs, watson_embedding, persist_directory=\"./db/multimodal_db\")"
],
"id": "96b45e3ca155e9ab",
"outputs": [],
"execution_count": 34
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:43.691778483Z",
"start_time": "2026-06-15T03:32:43.682586147Z"
}
},
"cell_type": "code",
"source": [
"# 검색결과에 이미지 포함 여부 확인\n",
"\n",
"def search_with_images(vectorstore, query):\n",
" results = vectorstore.similarity_search_with_relevance_scores(query, k=5)\n",
" text_results = [doc for doc, score in results if doc.metadata.get(\"type\") != 'image']\n",
" image_results = [doc for doc, score in results if doc.metadata.get(\"type\") == 'image']\n",
" print(f\"텍스트 결과 : {len(text_results)}, 이미지 결과 {len(image_results)}\")\n",
" return text_results, image_results"
],
"id": "8d26ef630c80b820",
"outputs": [],
"execution_count": 35
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:44.606069502Z",
"start_time": "2026-06-15T03:32:43.694865760Z"
}
},
"cell_type": "code",
"source": [
"image_doc = Document(\n",
" page_content=caption,\n",
" metadata = {\"type\" : \"image\", \"image_path\":\"./image/chart1.png\"}\n",
")\n",
"\n",
"docs = [\n",
" Document(\n",
" page_content=\"2025년 매출은 증가했다.\"\n",
" ),\n",
" image_doc\n",
"]\n",
"\n",
"build_multimodal_index(\"./image\", docs)"
],
"id": "a8beb26afaf51f76",
"outputs": [],
"execution_count": 36
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:46.252600621Z",
"start_time": "2026-06-15T03:32:44.616669574Z"
}
},
"cell_type": "code",
"source": [
"vectorstore = Chroma(embedding_function=watson_embedding, persist_directory=\"./db/multimodal_db\")\n",
"\n",
"results = vectorstore.similarity_search(\"매출 추세\", k=3)\n",
"\n",
"for r in results:\n",
" print(r.page_content)"
],
"id": "16976c61e211677c",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"2025년 매출은 증가했다.\n",
"2024년 분기별 매출 현황 막대그래프. Q1 120백만원(파랑), Q2 185백만원(초록), Q3 160백만원(주황), Q4 230백만원(빨강)으로 Q4가 최고치를 기록. 상단 좌측에 \"Q4 매출액 최고치\" 데이터 분석 메모 포함. 단위는 백만원.\n",
"최근 6개월 화장품 수출액 및 전년동기 대비(YoY) 증감률 추이를 보여주는 막대·선 혼합 차트. 25년 12월부터 26년 5월(1~20일)까지 수출액은 752.1~1096.3백만 달러 범위, YoY는 33.7%에서 -16%까지 변동. 4월 수출액 1096.3백만 달러로 최고, 5월 -16%로 최저 성장률 기록.\n"
]
}
],
"execution_count": 37
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:32:46.278685884Z",
"start_time": "2026-06-15T03:32:46.264136623Z"
}
},
"cell_type": "code",
"source": [
"def multimodal_answer(vectorstore, question):\n",
" text_results, image_results = search_with_images(vectorstore = vectorstore, query = question)\n",
"\n",
" # 텍스트 결과 하나의 컨텍스트로 생성\n",
" text_context = \"\\n\\n\".join([r.page_content for r in text_results])\n",
"\n",
" # 이미지로 검색된 경우\n",
" image_context = \"\"\n",
" referenced_images = []\n",
" for img_doc in image_results[:3]:\n",
" img_path = img_doc.metadata.get(\"image_path\")\n",
"\n",
" img_b64 = encode_image(img_path)\n",
" analysis = vision_llm.invoke([\n",
" HumanMessage(\n",
" content=[\n",
" {\"type\": \"image_url\", \"image_url\": {'url': f'data:image/jpeg;base64,{img_b64}'}},\n",
" {\"type\": \"text\", \"text\": f\"이 이미지에서 다음 질문과 관련된 내용을 설명하세요: {question}\"}\n",
" ]\n",
" )\n",
" ]).content\n",
"\n",
" image_context += f\"[이미지 분석: {img_path}]\\n{analysis}\\n\\n\"\n",
" referenced_images.append(img_path)\n",
"\n",
" # 최종 답변\n",
" combined_context = text_context + \"\\n\\n\" + image_context\n",
"\n",
" final_prompt = ChatPromptTemplate.from_messages([\n",
" (\"system\", \"다음은 텍스트와 이미지 분석 결과를 참고하여 질문에 대한 답변입니다.\\n\\n{context}\"),\n",
" (\"human\", \"{question}\")\n",
" ])\n",
"\n",
" parser = StrOutputParser()\n",
" chain = final_prompt | watson_llm | parser\n",
"\n",
" answer = chain.invoke({\n",
" \"context\": combined_context,\n",
" \"question\": question\n",
" })\n",
"\n",
" return {\"answer\" : answer, \"images\": referenced_images}"
],
"id": "4a82feae327cb3a1",
"outputs": [],
"execution_count": 38
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-06-15T03:33:23.098722925Z",
"start_time": "2026-06-15T03:32:46.280058605Z"
}
},
"cell_type": "code",
"source": "multimodal_answer(vectorstore, \"최근 6개월 전체 화장품 수출액?\")",
"id": "e0dbe6998b4f75fd",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"텍스트 결과 : 2, 이미지 결과 2\n"
]
},
{
"data": {
"text/plain": [
"{'answer': '최근 6개월 전체 화장품 수출액은 약 52억 달러입니다. 이는 6개월 동안의 월별 수출액을 합산한 결과입니다.',\n",
" 'images': ['./image/chart1.png', 'sample_images/sales_chart_2024.jpg']}"
]
},
"execution_count": 39,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 39
},
{
"metadata": {},
"cell_type": "code",
"source": "",
"id": "bce01c1bf17b40e1",
"outputs": [],
"execution_count": null
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}