BioPlayground

🧬
목록으로

INCI 성분 원칙 기반 뷰티 어드바이저 — 함량 순 표기 규칙 하나로 개인화 화장품 추천 실전

화장품 성분 함량 순 표기 규제(EU 1223/2009 · US 21 CFR 701 · KR 화장품법) 원칙을 활용해 오픈 데이터(Open Beauty Facts + CosIng)로 개인화 화장품 추천 도구 구축. 안면 병변 분할 CNN + chemical fingerprint 성분 임베딩 + 상충 룰(레티놀·AHA·비타민C) + Claude LLM 요약. SNP 유전 정보 배제 (GDPR Art.9 회피).

중급
|
40
|
검증 완료 (2026-07)
진행률0/15 (0%)

INCI 성분 원칙 기반 뷰티 어드바이저 — 함량 순 표기 규칙 하나로 개인화 화장품 추천 실전

화장품 성분 표기(INCI)는 대부분 국가에서 함량 순으로 나열됩니다. 이 규정 하나가 강력한 신호를 줍니다. 첫 3~5개 성분이 사실상 그 제품의 정체성을 결정하고, 1% 이하는 순서 자유이나 어떤 성분이 존재하는지는 확실합니다. 이 편은 이 규제 원칙을 활용해 개인화 화장품 추천 도구를 오픈 데이터와 AI 기법으로 실전 구축합니다. 예시 사용자는 "35세 여성, 지성 피부, 여드름·모공 개선 원함, 레티놀 처음 사용, 임신 계획 있음"이라는 실전 페르소나. 유전 정보(SNP)는 개인정보 규제 회피를 위해 배제하고, 성분 임베딩·안면 병변 CNN·상충 룰·LLM 요약을 조합합니다.

📚 선수 편 권고 (강력 권고)

이 편은 AI×바이오 하드코어 심화 편입니다. 진입 전에 DryBench의 다음 편들을 먼저 듣고·보시길 강력히 권고드립니다.

선수 편 없이 진입할 경우, 이 편에서 다루는 RAG 검색 · tool 오케스트레이션 · 임베딩 모델 로딩·CNN 세그멘테이션 원리에 대한 재설명 없이 실전 코드부터 진행하므로 따라가기 어렵습니다.


우리 DryBench에서 이거 배웠잖아

DryBench ai-native #8에서 RAG가 매개변수 밖 지식(문서·데이터베이스)을 검색 결과로 컨텍스트에 얹는 접근이라는 것을, #9에서 agent가 여러 tool을 자율 호출해 복합 태스크를 처리한다는 것을, #13에서 HuggingFace가 vision·화학 임베딩 모델을 표준 API로 제공한다는 것을 배웠습니다.

뷰티는 이 세 원리가 놀랍도록 자연스럽게 결합하는 도메인입니다. 화장품 성분 데이터베이스가 RAG 지식 소스, LLM이 사용자 피부·선호도와 성분 정보를 자연어로 매개, 화학 fingerprint 임베딩이 유사 성분 KNN 검색을 지원합니다. 이 편은 그 결합을 실전 도구로 만드는 파이프라인이며, 규제·개인정보 리스크를 회피하면서도 실용성을 잃지 않는 절충 설계까지 다룹니다.

하드코어 문제 정의

실전 사용자 페르소나

예시 페르소나로 파이프라인 요구를 구체화합니다.

  • 나이: 35세 여성.
  • 피부 타입: 지성 (T존 특히 심함).
  • 관심사: 여드름 관리 · 모공 축소 · 초기 노화 관리.
  • 사용 이력: 니아신아마이드 3개월 (효과 있음), BHA 1개월 (자극 있음).
  • 이번 신제품 후보: 레티놀 초심자용 세럼.
  • 특이 사항: 임신 계획 (retinoid 회피 필요) · salicylic acid 알레르기.
  • 예산: 브랜드 상관없이 성분·물성만.

이 페르소나 하나만으로도 파이프라인이 해결해야 할 과제가 많습니다.

  • 레티놀 후보 제품 리스트업 (INCI 상위 5개에 retinol · retinal · retinyl palmitate 중 하나 이상).
  • 임신 계획이므로 retinoid는 사실 회피 대상 → 대체 (bakuchiol 등 plant-based retinol alternative) 추천.
  • BHA(salicylic acid) 알레르기 명시 → 그 성분 포함 제품 즉시 필터.
  • 니아신아마이드 계속 사용 중 → 성분 시너지 있는 제품 우선.
  • 지성 피부 · 여드름 관리 → 콜레스테롤 · 마이르시틴 계열 · 지루성 유발 가능 성분(코코넛 오일 등) 회피.

규제 원칙이 만드는 정보 구조

화장품 INCI 표기의 핵심 원칙:

  • EU (Regulation EC 1223/2009): 함량 순 나열, 1% 이하 성분은 임의 순 배치 가능. 알레르기 유발 26종 별표 표기 [1].
  • US (21 CFR 701): 함량 순, 1% 이하 순서 무관, color additive 별도 표기 [2].
  • KR (화장품법 · 식약처): 함량 순, 1% 이하 순서 무관, 알레르기 유발 26종 EU 준함 (2020 개정) [3].
  • JP (의약외품 · 화장품 표시기준): All Ingredients Labeling, 함량 순 [4].

핵심 시사: 성분 배열 순서 자체가 정보. 특히 상위 5개는 사실상 그 제품의 정체성. 이 원칙 하나로 다음이 가능:

  • 주요 활성 성분 식별: 상위 성분에서 hyaluronic acid · niacinamide 등 활성 성분 발견 여부.
  • 필러 vs 실질 활성 구분: 상위에 water · glycerin · butylene glycol만 있으면 필러 비중 큼.
  • 상충 조합 감지: retinoid + AHA/BHA 조합 (자극 유발), vitamin C + retinol 조합 (안정성 저하) 등.

이 편의 실전 스코프

  • 오픈 데이터베이스에서 화장품 5만~10만 개 정보 수집 (Open Beauty Facts).
  • INCI 성분 각각 chemical fingerprint 임베딩 (RDKit ECFP).
  • 사용자 피부 프로파일 · 선호 성분 · 회피 성분 입력.
  • 안면 이미지 (선택) → CNN 병변 분할 → 지역별 관심 성분 매칭.
  • 룰 엔진으로 상충 성분 · 알레르기 · 임신 회피 성분 경고.
  • Claude LLM이 최종 추천 3개를 자연어 설명.

규제·개인정보 원칙 (엄수):

  • SNP · genotype 유전 정보 사용 X (KR 개인정보보호법 · EU GDPR Art.9 민감정보 회피).
  • 특정 브랜드 옹호/폄하 배제. 성분·물성·오픈 라벨만.
  • 안면 이미지는 로컬 처리 원칙 (외부 API 전송 회피). 사용자 동의 명시.
  • 의료 조언 아님 명시 (약사·의사·피부과 상담 권고 문구 필수 포함).
  • 임산부 · 알레르기 · 특정 지병 정보는 사용자 자율 입력, 프로파일링 X.

도구 스택과 인프라 요구

도구역할라이선스
Open Beauty Facts (오픈 데이터)화장품 성분·브랜드·바코드ODbL
CosIng (EU Commission)INCI 표준 사전공개
RDKitChemical fingerprint (ECFP · MACCS)BSD-3-Clause
HuggingFace transformers안면 CNN (Segformer 등)Apache 2.0
Claude API자연어 추천 요약상용
FAISS · scikit-learn유사 성분 KNN · 클러스터링MIT · BSD
FastAPI + Streamlit (선택)서비스화MIT · Apache 2.0
PubChem RESTCAS → SMILES 매핑공개

인프라 요구:

  • 소형 소비자 GPU (안면 CNN 추론). CPU 폴백 가능하지만 이미지 처리 10x 느림.
  • RAM 8GB 이상 (성분 임베딩 인덱스).
  • 디스크: Open Beauty Facts subset 약 500MB, CNN 모델 약 100MB.

학습자 재현 예상 비용: Claude API 요금표 기준 [5]으로 사용자 세션당 5~15센트. 오픈 데이터·CosIng 무료.

파이프라인 실전 구현

전체 흐름:

mermaid

Step 1. Open Beauty Facts 크롤 · INCI 파싱

Open Beauty Facts는 커뮤니티 기여 오픈 데이터 [6].

python
from dataclasses import dataclass, field
import gzip
import json
from pathlib import Path
import re
import requests
OBF_DUMP_URL = "https://static.openbeautyfacts.org/data/openbeautyfacts-products.jsonl.gz"
@dataclass
class Product:
barcode: str
brand: str
name: str
ingredients_raw: str
inci_list: list[str] # 함량 순으로 정렬된 성분 리스트
categories: list[str] = field(default_factory=list)
country: str = ""
image_url: str = ""
def download_obf_dump(dest: Path) -> Path:
"""Open Beauty Facts JSON dump 다운로드 (약 500MB gzip)."""
dest.parent.mkdir(parents=True, exist_ok=True)
if dest.exists() and dest.stat().st_size > 100_000_000:
return dest
print(f"다운로드 중... {OBF_DUMP_URL}")
with requests.get(OBF_DUMP_URL, stream=True, timeout=600) as r:
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=8192 * 1024):
f.write(chunk)
return dest
def parse_inci(ingredients_raw: str) -> list[str]:
"""INCI 성분 raw 텍스트를 함량 순 리스트로 파싱.
- 콤마·세미콜론·마침표 구분
- 소괄호 안은 alias · CI 번호 (예: "AQUA (WATER)""WATER")
- 대괄호 안은 제거 (nano 표기 등)
- 알레르기 유발 성분 별표 tag 유지
"""
if not ingredients_raw:
return []
# 정규화 · 소·대괄호 처리
parts = re.split(r"[,;·]", ingredients_raw)
cleaned = []
for p in parts:
# 대괄호 제거 (예: "[nano]")
p = re.sub(r"\[.*?\]", "", p)
# 소괄호 안 대체명을 별도 tag로 유지 (여기서는 통째 제거하지 않음)
p = re.sub(r"\(.*?\)", "", p)
p = p.strip().upper()
# 알레르기 별표 (**), 물결 (~) 등 특수 tag 유지 여부 결정
if p and len(p) > 1 and len(p) < 100:
cleaned.append(p)
return cleaned
def iter_cosmetics(dump_path: Path, min_ingredients: int = 3):
"""OBF dump 스트리밍 · 최소 성분 수 필터."""
with gzip.open(dump_path, "rt", encoding="utf-8") as f:
for line in f:
try:
d = json.loads(line)
except json.JSONDecodeError:
continue
if not d.get("ingredients_text"):
continue
inci = parse_inci(d.get("ingredients_text", ""))
if len(inci) < min_ingredients:
continue
yield Product(
barcode=d.get("code", ""),
brand=(d.get("brands") or "").split(",")[0].strip(),
name=d.get("product_name", ""),
ingredients_raw=d.get("ingredients_text", ""),
inci_list=inci,
categories=d.get("categories_tags", []),
country=d.get("countries", ""),
image_url=d.get("image_url", ""),
)

Step 2. CosIng 매칭 · 성분 정규화

INCI 이름은 표기 편차가 많음. CosIng을 사전으로 사용해 정규화 [7]. 부분 매칭 fallback 필수.

python
import csv
@dataclass
class CosingEntry:
inci_name: str
cas: str
einecs: str
function: str # e.g. "Skin conditioning · Emollient"
restriction: str # EU 규제 (예: "Concentration limit 0.5%")
allergen_flag: bool = False
def load_cosing_dict(cosing_csv: Path) -> dict[str, CosingEntry]:
"""CosIng CSV → {INCI_name(uppercased): CosingEntry} 사전."""
result: dict[str, CosingEntry] = {}
with open(cosing_csv, encoding="utf-8", errors="ignore") as f:
reader = csv.DictReader(f)
for row in reader:
key = (row.get("INCI name") or "").upper().strip()
if not key:
continue
result[key] = CosingEntry(
inci_name=key,
cas=row.get("CAS #", "").strip(),
einecs=row.get("EINECS/ELINCS #", "").strip(),
function=row.get("Function", "").strip(),
restriction=row.get("Restriction", "").strip(),
allergen_flag="allergen" in row.get("Restriction", "").lower(),
)
return result
def normalize_inci(raw_name: str, cosing_dict: dict[str, CosingEntry]) -> str | None:
"""CosIng 사전으로 정규화. 부분 매칭 fallback."""
upper = raw_name.upper().strip()
if upper in cosing_dict:
return upper
# 부분 매칭 (예: "AQUA (WATER)" → "AQUA")
for key in cosing_dict:
if key in upper or upper in key:
return key
# Fuzzy matching (Levenshtein distance) 대안
return None

Step 3. 성분 Chemical Fingerprint 임베딩

INCI 성분은 대부분 CAS 번호로 chemical structure 조회 가능. RDKit으로 fingerprint 계산.

python
from rdkit import Chem, RDLogger
from rdkit.Chem import AllChem
import numpy as np
RDLogger.DisableLog("rdApp.*")
def cas_to_smiles(cas_number: str, cache: dict | None = None) -> str | None:
"""CAS → SMILES. PubChem REST API [8]. 캐시 활용 권장 (반복 방지)."""
if not cas_number:
return None
if cache and cas_number in cache:
return cache[cas_number]
url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{cas_number}/property/CanonicalSMILES/JSON"
try:
resp = requests.get(url, timeout=15)
if resp.status_code != 200:
if cache is not None:
cache[cas_number] = None
return None
data = resp.json()
smiles = data["PropertyTable"]["Properties"][0]["CanonicalSMILES"]
if cache is not None:
cache[cas_number] = smiles
return smiles
except (requests.RequestException, KeyError):
if cache is not None:
cache[cas_number] = None
return None
def compute_ecfp(smiles: str, radius: int = 2, n_bits: int = 2048) -> np.ndarray:
"""ECFP (Extended Connectivity Fingerprint) 계산. Morgan Fingerprint."""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return np.zeros(n_bits, dtype=np.uint8)
fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius, nBits=n_bits)
return np.array(fp, dtype=np.uint8)
def build_ingredient_embeddings(
cosing_dict: dict[str, CosingEntry],
cache_path: Path,
sleep_between: float = 0.2, # PubChem 예의 (초당 5 request 이하)
) -> dict[str, np.ndarray]:
"""INCI 성분 → ECFP 임베딩 사전. PubChem 캐시 활용."""
import time
if cache_path.exists():
return np.load(cache_path, allow_pickle=True).item()
embeddings: dict[str, np.ndarray] = {}
smiles_cache: dict[str, str | None] = {}
for i, (inci_name, info) in enumerate(cosing_dict.items()):
if i % 100 == 0:
print(f"[{i}/{len(cosing_dict)}] fingerprint 계산 중")
smiles = cas_to_smiles(info.cas, cache=smiles_cache)
if smiles:
embeddings[inci_name] = compute_ecfp(smiles)
time.sleep(sleep_between) # PubChem rate limit 준수
np.save(cache_path, embeddings, allow_pickle=True)
return embeddings

Step 4. 상충·규제·알레르기 룰 엔진

피부과·화장품 화학의 알려진 상충 조합 + EU 26종 알레르기 유발 물질 + 임산부 회피 성분 통합.

python
from typing import Literal, NamedTuple
Severity = Literal["info", "warn", "avoid"]
class Rule(NamedTuple):
kind: str # "conflict" · "allergen" · "pregnancy" · "user_avoid"
ingredient_a: str
ingredient_b: str | None # None if 단일 성분 룰
severity: Severity
reason: str
reference: str
context: dict[str, str] = {}
CONFLICT_RULES: list[Rule] = [
Rule(
kind="conflict",
ingredient_a="RETINOL",
ingredient_b="GLYCOLIC ACID",
severity="warn",
reason="레티노이드 + AHA 동시 사용은 자극·홍반 유발 가능. 밤·아침 분리 사용 권장.",
reference="AAD (American Academy of Dermatology) retinoid guidelines",
),
Rule(
kind="conflict",
ingredient_a="RETINOL",
ingredient_b="SALICYLIC ACID",
severity="warn",
reason="레티노이드 + BHA 동시 사용은 자극 · 각질 파괴 과잉 유발 가능.",
reference="Journal of the American Academy of Dermatology",
),
Rule(
kind="conflict",
ingredient_a="RETINOL",
ingredient_b="ASCORBIC ACID",
severity="warn",
reason="레티놀 + 순수 비타민 C는 pH 요구가 상충 (retinol 5.5, vit C 3.5). 안정성·자극 이슈.",
reference="Skin Therapy Letter, vitamin C stability review",
),
Rule(
kind="conflict",
ingredient_a="BENZOYL PEROXIDE",
ingredient_b="RETINOL",
severity="avoid",
reason="BPO가 retinol을 산화·분해 → retinol 효과 소실 + 피부 자극.",
reference="Journal of Drugs in Dermatology",
),
Rule(
kind="conflict",
ingredient_a="NIACINAMIDE",
ingredient_b="ASCORBIC ACID",
severity="info",
reason="고농도 동시 사용 시 nicotinic acid 생성 이론. 최근 연구는 대체로 문제 없음 (논쟁적).",
reference="Cosmetic Dermatology reviews · 2024 update",
),
]
PREGNANCY_AVOID: list[Rule] = [
Rule(
kind="pregnancy", ingredient_a=name, ingredient_b=None,
severity="avoid",
reason=f"{name}는 임신 중 회피 권고 (기형 발생 위험 근거 or 예방적 회피).",
reference="ACOG (American College of Obstetricians and Gynecologists) guidelines",
)
for name in [
"RETINOL", "RETINALDEHYDE", "RETINYL PALMITATE", "TRETINOIN",
"ADAPALENE", "TAZAROTENE", # retinoid 계열
"SALICYLIC ACID", # BHA 고농도
"HYDROQUINONE", # 미백
]
]
EU_ALLERGENS_26 = [
"LIMONENE", "LINALOOL", "CITRAL", "GERANIOL", "EUGENOL", "COUMARIN",
"CITRONELLOL", "FARNESOL", "BENZYL ALCOHOL", "BENZYL BENZOATE",
"BENZYL SALICYLATE", "BENZYL CINNAMATE", "CINNAMAL", "CINNAMYL ALCOHOL",
"AMYLCINNAMAL", "AMYLCINNAMYL ALCOHOL", "HEXYL CINNAMAL",
"HYDROXYCITRONELLAL", "HYDROXYISOHEXYL 3-CYCLOHEXENE CARBOXALDEHYDE",
"ISOEUGENOL", "METHYL 2-OCTYNOATE", "ANISYL ALCOHOL", "ALPHA-ISOMETHYL IONONE",
"EVERNIA PRUNASTRI EXTRACT", "EVERNIA FURFURACEA EXTRACT",
"3-P-CUMENYL-2-METHYLPROPIONALDEHYDE",
]
def check_all_rules(
inci_list: list[str],
user_profile: dict,
) -> list[Rule]:
"""제품 성분 vs 룰 세트 → 관련 룰 리턴."""
triggered = []
upper_set = {name.upper() for name in inci_list}
# 1. 상충 조합
for rule in CONFLICT_RULES:
if rule.ingredient_a in upper_set and (rule.ingredient_b or "") in upper_set:
triggered.append(rule)
# 2. 임신 계획
if user_profile.get("pregnancy_planning", False):
for rule in PREGNANCY_AVOID:
if rule.ingredient_a in upper_set:
triggered.append(rule)
# 3. 사용자 알레르기 · 회피 성분
for avoid in user_profile.get("allergies", []) + user_profile.get("avoid_ingredients", []):
if avoid.upper() in upper_set:
triggered.append(Rule(
kind="user_avoid",
ingredient_a=avoid.upper(),
ingredient_b=None,
severity="avoid",
reason=f"사용자 자율 입력: {avoid} 회피 · 알레르기.",
reference="user profile",
))
# 4. EU 26 알레르기 유발 물질 (경고 수준)
for allergen in EU_ALLERGENS_26:
if allergen in upper_set:
triggered.append(Rule(
kind="allergen",
ingredient_a=allergen,
ingredient_b=None,
severity="info",
reason=f"EU 26 알레르기 유발 물질 목록 포함 (반드시 알레르기 발생은 아님).",
reference="EU Regulation (EC) 1223/2009 Annex III",
))
return triggered

Step 5. 안면 병변 분할 CNN (선택적)

사용자가 안면 이미지를 제공하면 병변(홍반·여드름·건조 영역)을 분할.

python
import torch
from transformers import SegformerImageProcessor, SegformerForSemanticSegmentation
class FacialLesionSegmenter:
"""Segformer 기반 안면 병변 분할 (개념 예시).
실전은 skin condition 특화 fine-tuned 모델 권장 (예: ISIC dermatology 벤치 fine-tune).
이 예시는 자연 이미지 학습 baseline. 실제 서비스에서는 필수 재학습.
"""
MODEL_ID = "nvidia/segformer-b0-finetuned-ade-512-512" # 예시, 실제는 skin-specific
def __init__(self, device: str = "cuda"):
self.device = device
self.processor = SegformerImageProcessor.from_pretrained(self.MODEL_ID)
self.model = SegformerForSemanticSegmentation.from_pretrained(self.MODEL_ID).to(device).eval()
@torch.no_grad()
def segment(self, image: np.ndarray) -> dict:
"""RGB 이미지 → 병변 마스크 dict."""
inputs = self.processor(images=image, return_tensors="pt").to(self.device)
outputs = self.model(**inputs)
logits = outputs.logits
upsampled = torch.nn.functional.interpolate(
logits, size=image.shape[:2], mode="bilinear", align_corners=False
)
pred_mask = upsampled.argmax(dim=1).squeeze().cpu().numpy()
return {
"mask": pred_mask,
"class_ratios": {
int(cls): float((pred_mask == cls).mean())
for cls in np.unique(pred_mask)
},
}
LESION_TO_INGREDIENT_HINTS = {
"acne": ["SALICYLIC ACID", "BENZOYL PEROXIDE", "NIACINAMIDE", "ZINC PCA"],
"erythema": ["CENTELLA ASIATICA EXTRACT", "MADECASSOSIDE", "PANTHENOL"],
"dryness": ["HYALURONIC ACID", "GLYCERIN", "CERAMIDE NP", "SQUALANE"],
"hyperpigmentation": ["NIACINAMIDE", "ALPHA-ARBUTIN", "TRANEXAMIC ACID", "AZELAIC ACID"],
}

중요 규제 문구 (편 본문에 명시): 위 CNN 모델은 자연 이미지 학습 baseline. 실제 서비스는 dermatology 특화 fine-tuned 모델 필수. 그리고 진단 아님 (의료 기기 규제 회피 · 사용자에게 명시).

Step 6. Claude LLM 자연어 추천

python
import anthropic
RECOMMEND_PROMPT = """당신은 화장품 성분·규제 전문가 어시스턴트입니다.
사용자 프로필과 후보 제품을 검토해 상위 3개를 자연어로 추천합니다.
사용자 프로필:
- 피부 타입: {skin_type}
- 관심 성분: {preferred}
- 회피 성분 · 알레르기: {avoid}
- 임신 계획: {pregnancy}
- 사용 이력: {history}
후보 제품 (INCI 상위 5 성분):
{products_formatted}
감지된 룰 (상충 · 알레르기 · 임신 회피):
{rules_formatted}
원칙:
1. 특정 브랜드 옹호 · 폄하 배제. 성분 · 물성 · 룰 근거만.
2. 각 추천에 짧은 이유(3~5문장): 어느 상위 성분이 사용자 목적에 맞는지 · 룰 감지 시 어떻게 대처했는지.
3. 반드시 하단에 "본 정보는 참고용이며 의료 조언이 아닙니다. 피부 문제는 피부과 전문의 상담을 권합니다." 포함.
4. 임신 계획이 있는 사용자에게 retinoid 관련 제품이 후보에 있으면 반드시 회피 권고 후 대체(bakuchiol 등) 제안.
응답 형식: Markdown.
"""
def format_products(products: list[dict]) -> str:
lines = []
for i, p in enumerate(products, 1):
top5 = ", ".join(p["inci_list"][:5])
lines.append(f"{i}. {p['brand']} / {p['name']}\n 상위 5 성분: {top5}")
return "\n".join(lines)
def format_rules(rules: list[Rule]) -> str:
if not rules:
return "감지된 룰 없음."
lines = []
for r in rules:
if r.ingredient_b:
pair = f"{r.ingredient_a} × {r.ingredient_b}"
else:
pair = r.ingredient_a
lines.append(f"- [{r.severity}] {pair}: {r.reason} (출처: {r.reference})")
return "\n".join(lines)
def generate_recommendation(
user_profile: dict,
top_products: list[dict],
all_rules: list[Rule],
model: str = "claude-sonnet-4-5",
) -> str:
"""Claude가 최종 추천 자연어 요약."""
client = anthropic.Anthropic()
prompt = RECOMMEND_PROMPT.format(
skin_type=user_profile.get("skin_type", ""),
preferred=", ".join(user_profile.get("preferred", [])),
avoid=", ".join(user_profile.get("avoid_ingredients", []) + user_profile.get("allergies", [])),
pregnancy="예" if user_profile.get("pregnancy_planning", False) else "아니오",
history="; ".join(user_profile.get("history", [])),
products_formatted=format_products(top_products),
rules_formatted=format_rules(all_rules),
)
resp = client.messages.create(
model=model,
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
return resp.content[0].text

Step 7. 통합 파이프라인 · 페르소나 실행 예시

python
def full_advisor_pipeline(
user_profile: dict,
face_image: np.ndarray | None,
product_db: list[Product],
ingredient_embeddings: dict[str, np.ndarray],
top_k: int = 10,
) -> dict:
"""사용자 프로필 + 안면 이미지 → 추천 제품 + 자연어 설명 + 룰 리포트."""
# 1. 사용자 선호 성분 임베딩 평균
preferred = [p.upper() for p in user_profile.get("preferred", [])]
pref_embs = [ingredient_embeddings[p] for p in preferred if p in ingredient_embeddings]
user_emb = np.mean(pref_embs, axis=0) if pref_embs else None
# 2. 회피 성분 · 알레르기 · 임신 회피 성분 통합
hard_avoid = {a.upper() for a in user_profile.get("avoid_ingredients", []) + user_profile.get("allergies", [])}
if user_profile.get("pregnancy_planning", False):
for rule in PREGNANCY_AVOID:
hard_avoid.add(rule.ingredient_a)
# 3. 후보 스코어링
scored = []
for prod in product_db:
top5 = {i.upper() for i in prod.inci_list[:10]}
# Hard avoid 있으면 즉시 배제
if hard_avoid & top5:
continue
top5_embs = [
ingredient_embeddings[i] for i in prod.inci_list[:5]
if i in ingredient_embeddings
]
if not top5_embs:
continue
prod_emb = np.mean(top5_embs, axis=0)
if user_emb is not None:
similarity = float(np.dot(prod_emb, user_emb) / (
np.linalg.norm(prod_emb) * np.linalg.norm(user_emb) + 1e-8
))
else:
similarity = 0.5
scored.append({
"brand": prod.brand, "name": prod.name,
"inci_list": prod.inci_list, "score": similarity,
"categories": prod.categories, "image_url": prod.image_url,
})
# 4. 상위 K개 정렬
scored.sort(key=lambda x: -x["score"])
top_products = scored[:top_k]
# 5. 룰 검사
all_rules = []
for prod in top_products:
all_rules.extend(check_all_rules(prod["inci_list"], user_profile))
# 6. 안면 병변 분석 (있으면)
lesion_info = None
if face_image is not None:
segmenter = FacialLesionSegmenter()
lesion_info = segmenter.segment(face_image)
# 7. Claude 자연어 추천
recommendation = generate_recommendation(user_profile, top_products, all_rules)
return {
"recommended_products": top_products,
"triggered_rules": [r._asdict() for r in all_rules],
"lesion_analysis": lesion_info,
"recommendation_text": recommendation,
"disclaimer": "본 정보는 참고용이며 의료 조언이 아닙니다. 피부 문제는 피부과 전문의 상담을 권합니다.",
}
# 실행 예시 (페르소나 시나리오)
# user = {
# "skin_type": "oily · acne-prone",
# "preferred": ["NIACINAMIDE", "BAKUCHIOL", "ZINC PCA"],
# "avoid_ingredients": ["ALCOHOL DENAT"],
# "allergies": ["SALICYLIC ACID"],
# "pregnancy_planning": True,
# "history": ["니아신아마이드 3개월 효과 있음", "BHA 1개월 자극"],
# }
# result = full_advisor_pipeline(user, face_image=None, product_db=[...], ingredient_embeddings={...})
# print(result["recommendation_text"])

성능·비용·알려진 실패 케이스

성능 참고 (공개 근거 인용)

이 편의 완전한 벤치마크는 오픈 데이터 성격상 gold standard가 부재하지만, 각 컴포넌트별 공개 근거:

컴포넌트벤치지표출처
ECFP fingerprintChEMBL 유사도화학 유사도 상관 r ≈ 0.75Rogers & Hahn 2010 [9]
Segformer (자연 이미지)ADE20KmIoU 0.65Xie et al., NeurIPS 2021 [10]
Segformer (skin fine-tuned)ISIC 벤치Dice 0.80~0.85커뮤니티 fine-tune 벤치 [11]
Claude summarization벤치 없음사용자 선호 A/B 테스트 필요
Open Beauty Facts 데이터 정합성자체 검증INCI 표기 오류율 ~15%OBF 커뮤니티 리포트 [6]

학습자 재현 예상 비용

  • Claude API: 사용자 세션당 약 5~15센트.
  • CAS → SMILES PubChem API: 무료 (요청 사이 대기 필수, 초당 5 req 이하).
  • 안면 CNN 추론: 로컬 GPU 기준 이미지 1장 100~500ms.
  • 전체 시스템 자체 호스팅: 소형 VPS 월 5~20 USD.

알려진 실패 케이스 5건 (커뮤니티·논문 수집형)

  1. INCI 표기 편차·오타로 CosIng 매칭 실패
    증상: 제품 라벨 INCI가 CosIng 표준 이름과 미묘하게 다름 ("AQUA" vs "AQUA (WATER)" vs "WATER").
    원인: 브랜드마다 표기 관행 차이, 다국어 병기, 오타.
    회피: (a) 부분 매칭 fallback (본 편 Step 2), (b) fuzzy matching (Levenshtein distance, rapidfuzz 라이브러리), (c) 커뮤니티 대체명 사전 (OBF INCI dictionary 활용), (d) LLM 정규화 (Claude로 "이 표기는 INCI 어느 성분?" 질의).
    출처: Open Beauty Facts GitHub — INCI dictionary 이슈 [6].

  2. CAS → SMILES 매핑 부재 (특히 자연 유래 성분)
    증상: 식물 추출물 · 발효 산물 · 프리미엄 essential oil은 CAS가 하나로 정의 안 되거나 PubChem에 SMILES 없음.
    원인: 자연 유래 물질은 여러 화합물 혼합. 단일 SMILES 부적절.
    회피: (a) 자연 유래 성분은 별도 카테고리로 분리, (b) MACCS keys 대신 텍스트 임베딩 (LLM으로 성분 설명 임베딩) 대체, (c) 대표 활성 성분만 fingerprint (예: Centella Asiatica → madecassoside).
    출처: PubChem "natural products" documentation [8].

  3. 상충 룰의 과잉 경고 vs 개인차 무시
    증상: retinol + niacinamide 상충 경고를 자동 표시했는데 최근 연구는 그 조합이 실제 문제 없다고 반박. 사용자 혼란.
    원인: 화장품 화학 연구는 빠르게 갱신되며 dermatology 커뮤니티 내에서도 이견.
    회피: (a) 룰 신뢰도 표기 ("확실"/"논쟁적"), (b) 참고 문헌 URL 병기, (c) 사용자가 룰 dismiss 가능 UI, (d) 룰 사전 정기 갱신 (예: 분기별 리뷰), (e) severity "info" 수준은 UI에서 접힘 기본값.
    출처: Journal of Cosmetic Dermatology, niacinamide-vitamin C debate 리뷰 [12].

  4. 안면 CNN 도메인 shift (조명 · 인종 · 각도)
    증상: 자연 이미지 학습 CNN이 사용자 셀카(스마트폰 조명·앵글 다양)에 성능 급락.
    원인: 학습 데이터의 lighting · demographic diversity 부족. 특정 인종 데이터셋 편향 (예: ISIC은 백인 피부 편중).
    회피: (a) 다양한 인종·조명 데이터로 fine-tuning, (b) 사용자에게 조명·각도 가이드 UI 제시, (c) 결과에 신뢰도 표기, (d) 우선순위 낮게 (핵심 로직은 성분 임베딩·룰 엔진).
    출처: Wen et al. "Characteristics of publicly available skin cancer image datasets: a systematic review." Lancet Digital Health 2022 [13].

  5. 개인정보 처리 규제 (KR 개인정보보호법 · GDPR · CCPA)
    증상: 안면 이미지 · 알레르기 프로필 · 임신 여부 저장 시 규제 위반 위험.
    원인: 안면 이미지는 KR 개인정보보호법상 생체정보, GDPR Art.9 특별 카테고리. 임신 여부는 건강 정보 (GDPR 특별 카테고리).
    회피: (a) 안면 이미지는 로컬 브라우저 처리만 (서버 전송 X), (b) 사용자 프로필은 세션 내에서만, DB 저장 시 명시적 동의 · 암호화, (c) 프로필링 X · 광고 활용 X 명시, (d) 개인정보 처리방침 · 동의서 필수 UI, (e) 아동(만 14세 이하) 사용 제한.
    출처: 개인정보보호위원회 화장품·뷰티 앱 가이드; GDPR Art.9 · Art.22.

확장 아이디어

  • 바코드 스캔 앱 통합: 매장에서 사용자가 바코드 스캔 → 즉시 성분·매칭·경고.
  • 다국어 라벨 OCR: 해외 화장품 라벨 사진 → Tesseract OCR → 자동 INCI 파싱.
  • 알레르기 프로필 확장: 사용자가 알레르기 원인 물질 등록 → 그 물질 함유 제품 즉시 필터 + 유사 성분(fingerprint 유사도)도 경고.
  • 환경 지속가능성 필터: 마이크로플라스틱 · 팜유 · 동물 실험 여부 tag.
  • 가격 대비 성분 밀도 스코어: 활성 성분 상위 배치 vs 가격 상관관계.
  • 커뮤니티 리뷰 감성 분석: OBF 리뷰 · Reddit r/SkincareAddiction 스크래핑 (라이선스 유의) · LLM 감성 분석.
  • 한국 특화 KFDA 심사 대상 표시: 기능성 화장품 인증(주름개선 · 미백 · 자외선) 태그.

다음 편

  • 편 07 drug-target-gnn: GNN으로 성분-피부수용체 상호작용 정량화.
  • 편 09 llm-vendor-benchmark: 이 편의 자연어 추천을 Claude · GPT · Gemini로 벤치마크.
  • 편 14 bio-mcp-agent: 이 파이프라인을 MCP tool로 노출 → 챗봇형 뷰티 어드바이저.

참고 문헌

  1. EU Regulation (EC) No 1223/2009 (Cosmetic Products Regulation): https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02009R1223
  2. FDA Cosmetic Labeling Guide (21 CFR 701): https://www.fda.gov/cosmetics/cosmetics-labeling
  3. 대한민국 화장품법 (식약처): https://www.mfds.go.kr/
  4. Japan 의약외품·화장품 표시기준: https://www.mhlw.go.jp/
  5. Anthropic Claude API pricing: https://www.anthropic.com/pricing
  6. Open Beauty Facts: https://world.openbeautyfacts.org/ / GitHub: https://github.com/openfoodfacts/openbeautyfacts-server
  7. CosIng (EU Commission Cosmetic Ingredients Database): https://ec.europa.eu/growth/tools-databases/cosing/
  8. PubChem REST API documentation: https://pubchem.ncbi.nlm.nih.gov/docs/pug-rest
  9. Rogers D, Hahn M. "Extended-Connectivity Fingerprints." J Chem Inf Model 2010.
  10. Xie E, Wang W, Yu Z, et al. "SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers." NeurIPS 2021. https://arxiv.org/abs/2105.15203
  11. ISIC (International Skin Imaging Collaboration): https://www.isic-archive.com/
  12. Journal of Cosmetic Dermatology (niacinamide+vitamin C debate reviews): 여러 논문
  13. Wen D, Khan SM, Xu AJ, et al. "Characteristics of publicly available skin cancer image datasets: a systematic review." Lancet Digital Health 2022. https://www.thelancet.com/journals/landig/article/PIIS2589-7500(21)00252-1
  14. American Academy of Dermatology guidelines: https://www.aad.org/
  15. American College of Obstetricians and Gynecologists (ACOG) guidelines: https://www.acog.org/
  16. GDPR Art.9 (special categories): https://gdpr-info.eu/art-9-gdpr/
  17. RDKit Fingerprints documentation: https://www.rdkit.org/docs/GettingStartedInPython.html
  18. rapidfuzz (fuzzy matching): https://github.com/rapidfuzz/RapidFuzz
  19. INCI Beauty (커뮤니티 참고): https://incibeauty.com/
  20. EWG Skin Deep (참고, 방법론 논쟁적): https://www.ewg.org/skindeep/