INCI成分リストに基づいた美容アドバイザー:パーソナライズされた化粧品推奨のための実践的なガイド
ほとんどの国では、化粧品成分表示(INCI)において、成分を濃度が高い順にリストすることが原則として定められています。この単一の規制は、強力なシグナルを提供します。最初の3〜5つの成分は、製品の特性をほぼ決定づけており、1%未満の成分の順序は柔軟ですが、特定の成分の存在は明確に示されます。本稿では、この規制原則を活用し、オープンデータとAI技術を用いて、実用的なパーソナライズされた化粧品推奨ツールを構築します。ここでは、「35歳の女性で、肌質は脂性、ニキビの改善と毛穴の引き締めを希望しており、レチノールは初めて使用するが、妊娠を計画中」というユーザーを例に説明します。個人情報保護に関する規制の問題を回避するために、遺伝情報(SNP)は除外し、代わりに成分の埋め込み、顔の病変のCNN、競合ルール、およびLLMによる要約を組み合わせます。
📚 推奨される予備知識(強く推奨)
本稿は、AIと生物学に関する高度で詳細な解説です。先にDryBenchの以下の記事を確認することを強くお勧めします。
- DryBench ai-native #8 RAGとコンテキスト
- DryBench ai-native #9 エージェントとツールの利用
- DryBench ai-native #13 HuggingFaceと商用API
予備知識を確認せずに読み進めると、本稿で提示する実践的なコード例を理解するのが難しくなる可能性があります。なぜなら、RAG検索、ツールオーケストレーション、埋め込みモデルのロード、および以前の記事で説明したCNNセグメンテーションの原理について、再度説明は行わないからです。
DryBenchで学んだこと
DryBench ai-native #8では、RAGが外部ソース(ドキュメント、データベース)から知識を検索し、その知識を検索結果のコンテキストに組み込むアプローチであることを学びました。#9では、エージェントが複数のツールを自律的に呼び出して、複雑なタスクを実行できることを学びました。そして、#13では、HuggingFaceが標準APIを通じて、視覚および化学的埋め込みモデルを提供することを知りました。
美容は、これら3つの原理が驚くほど自然に組み合わさる分野です。化粧品成分データベースは、RAGのための知識源として機能し、LLMはユーザーの肌の状態/好みと成分情報を自然言語で仲介し、化学的指紋の埋め込みは、類似成分のKNN検索をサポートすることができます。本稿では、この組み合わせから実用的なツールを作成するためのパイプラインを紹介するとともに、規制およびプライバシーのリスクを回避しながら、実用性を維持するために必要なトレードオフについても説明します。
難題の定義
実用的なユーザーペルソナ
例となるペルソナを用いて、パイプラインの要件を定義しましょう。
- 年齢:35歳の女性。
- 肌質:脂性肌(特にTゾーン)。
- 興味:ニキビのケア、毛穴の縮小、そして早期のアンチエイジング。
- 使用履歴:ナイアシンアミド(3ヶ月、効果あり)、BHA(1ヶ月、刺激あり)。
- 新しい製品候補:初心者向けのレチノールセラム。
- 特記事項:妊娠を計画中(レチノイドの使用は避ける必要あり)、サリチル酸アレルギー。
- 予算:特定のブランドにこだわらず、成分とテクスチャを重視。
この単一のペルソナだけでも、パイプラインが対処する必要のある多くの課題が存在します。
- 潜在的なレチノール製品をリストアップする(上位5つのINCI成分の中に、レチノール、レチナール、またはレチニルパルミテートのいずれか1つを含む)。
- 妊娠の計画があるため、レチノイドは避けるべきであり、代替品を推奨する(例:バクチオールやその他の植物由来のレチノール代替品)。
- サリチル酸アレルギーであることを明示し、その成分を含む製品をすぐに除外する。
- ユーザーはすでにナイアシンアミドを使用しているため、ナイアシンアミドとの相乗効果がある製品を優先する。
- 脂性肌とニキビのケアのために、コレステロール、ミリスチルアルコール、ココナッツオイルなど、コメドジェニックである可能性のある成分を避ける。
規制原則によって構築された情報構造
化粧品INCI表示の主要な原則:
- **EU(規則EC 1223/2009):**成分は濃度が高い順にリストされ、1%未満の成分はランダムな順序でリストされる。26種類のアレルゲン成分にはアスタリスクが付けられる[1]。
- **米国(21 CFR 701):**成分は濃度が高い順にリストされ、1%未満の成分はランダムな順序でリストされる。着色料は別途リストされる[2]。
- **韓国(化粧品法、MFDS):**成分は濃度が高い順にリストされ、1%未満の成分はランダムな順序でリストされる。26種類のアレルゲン成分は、EU基準に従ってリストされる(2020年に改訂)[3]。
- **日本(医薬部外品および化粧品表示基準):**すべての成分がリストされ、濃度が高い順に並べられる[4]。
重要なポイント: 成分のリストの順序は、それ自体が情報です。特に、上位5つの成分は、製品のアイデンティティをほぼ決定します。この原則により、以下のことが可能になります。
- **主要な有効成分の特定:**ヒアルロン酸やナイアシンアミドなどの有効成分が、上位の成分に含まれているかどうかを判断する。
- **フィラーと有効成分の区別:**上位の成分が水、グリセリン、ブチレングリコールのみである場合、その製品にはフィラーが多く含まれている可能性が高い。
- **相反する組み合わせの検出:**例えば、レチノイドとAHA/BHA(刺激を引き起こす可能性がある)の組み合わせ、またはビタミンCとレチノール(安定性を低下させる可能性がある)の組み合わせなど。
本稿の対象範囲
- オープンデータベース(Open Beauty Facts)から、5万から10万件の化粧品のデータを収集する。
- 各INCI成分に対して、化学的特徴量埋め込み(RDKit ECFP)を作成する。
- ユーザーが自分の肌の状態、好みの成分、避けるべき成分を入力できるようにする。
- (オプション)顔画像とCNNを使用して、病変をセグメント化し、顔の特定の部分に関連する成分と一致させる。
- ルールエンジンを使用して、ユーザーに相反する成分、アレルギー、妊娠中に避けるべき成分について警告する。
- Claude LLMを使用して、上位3つの推奨製品について、自然言語で説明する。
規制およびプライバシーの原則(遵守):
- SNPまたは遺伝子型遺伝情報を使用しない(韓国の個人情報保護法およびEUのGDPR第9条に違反する可能性があるため、機密情報に該当)。
- 特定のブランドを宣伝または否定しない。成分、テクスチャ、および公開されている情報のみに焦点を当てる。
- 顔画像をローカルで処理し、外部APIに送信しない。明示的なユーザーの同意を得る。
- このツールは医学的なアドバイスを提供するものではなく、ユーザーに薬剤師、医師、または皮膚科医に相談することを促すことを明記する。
- ユーザーが、妊娠、アレルギー、および特定の病状に関する情報を自由に提供できるようにし、その情報に基づいてユーザーをプロファイリングしない。
ツール、スタック、およびインフラストラクチャ要件
| ツール | 役割 | ライセンス |
|---|---|---|
| Open Beauty Facts(オープンデータ) | 化粧品の成分、ブランド、バーコード | ODbL |
| CosIng(EU委員会) | INCI標準辞書 | パブリック |
| RDKit | 化学的特徴量(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 REST | CASからSMILESへのマッピング | パブリック |
インフラストラクチャ要件:
- 小さなコンシューマーGPU(顔CNNの推論用)。CPUによる代替も可能だが、画像処理は10倍遅くなる。
- 8GB以上のRAM(成分の埋め込みインデックス用)。
- ディスク:Open Beauty Factsのサブセット(約500MB)、CNNモデル(約100MB)。
推定学習コスト: Claude APIの価格に基づいて、各ユーザーセッションのコストは0.05ドルから0.15ドルと推定されます。オープンデータとCosIngは無料です。
実際のパイプラインの実装
全体の流れ:
ステップ1. Open Beauty Factsクローリング・INCI解析
Open Beauty Factsは、コミュニティによって運営されているオープンデータソースです[6]。
from dataclasses import dataclass, fieldimport gzipimport jsonfrom pathlib import Pathimport re
import requests
OBF_DUMP_URL = "https://static.openbeautyfacts.org/data/openbeautyfacts-products.jsonl.gz"
@dataclassclass 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ダンプ(約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の生のテキストを、濃度順のリストに解析します。
- カンマ、セミコロン、またはピリオドで区切ります。 - 括弧には、別名またはCI番号が含まれます(例:「AQUA (WATER)」の「WATER」)。 - 角かっこを削除します(例:ナノラベリング)。 - アレルギー誘発成分の「*」タグを保持します。 """ if not ingredients_raw: return [] # 正規化し、角かっこを処理します parts = re.split(r"[,;·]", ingredients_raw) cleaned = [] for p in parts: # 角かっこを削除します(例:「[nano]」) p = re.sub(r"\[.*?\]", "", p) # 括弧で囲まれた別名を別のタグに置き換えます(完全に削除はしません)。 p = re.sub(r"\(.*?\)", "", p) p = p.strip().upper() # 特殊なタグ(アレルギーのアスタリスク「**」、チルダ「~」など)を保持するかどうかを判断します。 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): """Open Beauty Factsダンプをストリーム処理し、最小成分数でフィルタリングします。""" 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", ""), )ステップ2. CosIngマッチング・成分正規化
INCI名は、表記に多くのバリエーションがあります。CosIngを辞書として使用して正規化します[7]。部分一致によるフォールバックが不可欠です。
import csv
@dataclassclass CosingEntry: inci_name: str cas: str einecs: str function: str # 例:「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(大文字)}: 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辞書を使用して正規化します。部分一致によるフォールバック。""" 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 # 代替案:ファジーマッチング(レーベンシュタイン距離) return Noneステップ3. 成分の化学的フィンガープリントエンベディング
ほとんどのINCI成分は、CAS番号で化学構造を検索できます。RDKitを使用してフィンガープリントを計算します。
from rdkit import Chem, RDLoggerfrom rdkit.Chem import AllChemimport 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(拡張接続性フィンガープリント)を計算します。モーガンフィンガープリント。""" 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に敬意を払いましょう(1秒あたり5回未満のリクエスト)。) -> 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)}] フィンガープリントを計算しています") smiles = cas_to_smiles(info.cas, cache=smiles_cache) if smiles: embeddings[inci_name] = compute_ecfp(smiles) time.sleep(sleep_between) # PubChemのレート制限を尊重します。
np.save(cache_path, embeddings, allow_pickle=True) return embeddingsステップ4. 競合、規制、アレルギールールエンジン
皮膚科および化粧品化学における、互換性のない成分の既知の組み合わせ、26種類のEUアレルゲン物質、および妊娠中の女性が避けるべき成分を統合します。
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の場合、単一成分のルール 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(アメリカ皮膚科学会)レチノイドガイドライン", ), 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要件が異なります(レチノール5.5、ビタミンC 3.5)。これにより、安定性が損なわれ、刺激を引き起こす可能性があります。", reference="Skin Therapy Letter、ビタミンCの安定性に関するレビュー", ), Rule( kind="conflict", ingredient_a="BENZOYL PEROXIDE", ingredient_b="RETINOL", severity="avoid", reason="BPOはレチノールを酸化させ、効果を低下させ、皮膚を刺激します。", reference="Journal of Drugs in Dermatology", ), Rule( kind="conflict", ingredient_a="NIACINAMIDE", ingredient_b="ASCORBIC ACID", severity="info", reason="高濃度の両方を組み合わせるとニコチン酸が生成されるという説があります。最近の研究では、これは問題ではないことが示されています(議論の余地があります)。", reference="Cosmetic Dermatology reviews · 2024年更新", ),]
PREGNANCY_AVOID: list[Rule] = [ Rule( kind="pregnancy", ingredient_a=name, ingredient_b=None, severity="avoid", reason=f"{name}は、妊娠中に避けることをお勧めします(出生欠損症の可能性を示す証拠があるため、または予防措置として)。", reference="ACOG(アメリカ産婦人科医協会)ガイドライン", ) for name in [ "RETINOL", "RETINALDEHYDE", "RETINYL PALMITATE", "TRETINOIN", "ADAPALENE", "TAZAROTENE", # レチノイドファミリー "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]: """製品の成分とルールセットに基づいて、関連するルールを返します。""" 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="ユーザープロファイル", ))
# 4. 26種類のEUアレルゲン物質(警告レベル) 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"26種類のEUアレルゲン物質のリストに含まれています(必ずしもアレルギー反応を意味するものではありません)。", reference="EU Regulation (EC) 1223/2009 Annex III", ))
return triggeredステップ5. 顔面病変セグメンテーションCNN(オプション)
ユーザーが顔画像を提供した場合、病変(赤み、ニキビ、乾燥した部分)をセグメント化します。
import torchfrom transformers import SegformerImageProcessor, SegformerForSemanticSegmentation
class FacialLesionSegmenter: """顔面病変セグメンテーション(Segformerベースの概念的な例)。
実際には、皮膚の状態に合わせて微調整されたモデル(例:ISIC皮膚科ベンチマークで微調整されたモデル)を使用することをお勧めします。 この例では、自然画像でトレーニングされたベースラインモデルを使用します。これは、実際には再トレーニングする必要があります。 """
MODEL_ID = "nvidia/segformer-b0-finetuned-ade-512-512" # 例。実際には、皮膚に特化したモデル。
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画像→病変マスク辞書。""" 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モデルは、自然画像でトレーニングされたベースラインです。実際には、皮膚に特化した微調整されたモデルが不可欠です。また、診断ツールではありません(医療機器としての規制を回避するために、これはユーザーに伝える必要があります)。
ステップ6. Claude LLM自然言語による推奨
import anthropic
RECOMMEND_PROMPT = """あなたは、化粧品成分と規制を専門とするAIアシスタントです。ユーザープロファイルと候補製品を確認し、上位3つの製品を自然言語で推奨します。
ユーザープロファイル:- 肌質:{skin_type}- 好みの成分:{preferred}- 避けるべき成分/アレルギー:{avoid}- 妊娠の計画:{pregnancy}- 使用履歴:{history}
候補製品(上位5つのINCI成分):{products_formatted}
検出されたルール(競合、アレルギー、妊娠回避):{rules_formatted}
原則:1. 特定のブランドを推奨または非難しないでください。成分、特性、ルールのみに基づいて推奨を行います。2. 各推奨には、短い説明(3〜5文)を含める必要があります。上位の成分がユーザーの目標にどのように一致するか、競合がどのように処理されたかを説明します。3. 常に最後に、次の文を含めます。「この情報は参考用であり、医学的なアドバイスではありません。皮膚の問題については、皮膚科医に相談してください。」4. ユーザーが妊娠を計画しており、候補製品にレチノイドが含まれている場合、レチノイドを避けるようにアドバイスし、代替品(例:バクチオール)を提案してください。
応答形式: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="Yes" if user_profile.get("pregnancy_planning", False) else "No", 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ステップ7. 統合パイプライン・ペルソナ実行例
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": "脂性・ニキビができやすい",# "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"])
## パフォーマンス、コスト、および既知の失敗事例
### パフォーマンスの参照 (公開ベンチマークに基づく)
このセクションでは、包括的なベンチマークを提供しますが、オープンデータであるため、ゴールドスタンダードは利用できません。以下の表に、各コンポーネントのパフォーマンス指標を示します。
| コンポーネント | ベンチマーク | 指標 | 出典 ||----------|------|------|------|| ECFPフィンガープリント | ChEMBL類似性 | 化学的類似性相関係数 r ≈ 0.75 | Rogers & Hahn 2010 [9] || Segformer (自然画像) | ADE20K | mIoU 0.65 | Xie et al., NeurIPS 2021 [10] || Segformer (スキン、ファインチューニング済み) | ISICベンチマーク | Dice 0.80〜0.85 | コミュニティによるファインチューニングベンチマーク [11] || Claudeによる要約 | ベンチマークなし | ユーザーの好みを考慮したA/Bテストが必要 | — || Open Beauty Factsデータの整合性 | 自己検証 | INCI表記のエラー率〜15% | OBFコミュニティレポート [6] |
### システムの複製にかかる推定コスト
- Claude API: ユーザーセッションあたり約5〜15セント。- CAS → SMILES PubChem API: 無料(リクエスト間に待機時間が必要、1秒あたり5リクエストに制限)。- 顔認識CNN推論: ローカルGPU上で1画像あたり100〜500ms。- システム全体のホスティング: 小規模なVPS、月額5〜20米ドル。
### 5つの既知の失敗事例 (コミュニティと論文から収集)
1. **CosIngマッチングの失敗 (INCI表記のバリエーションとタイプミスによる)** 症状: 製品ラベルのINCIが、CosIng標準名とわずかに異なる ("AQUA" vs "AQUA (WATER)" vs "WATER")。 原因: ブランド間の表記方法の違い、多言語ラベル、タイプミス。 緩和策: (a) 部分的なマッチングによるフォールバック (このドキュメントのステップ2)、(b) ファジーマッチング (レーベンシュタイン距離、`rapidfuzz`ライブラリ)、(c) コミュニティによる代替名称辞書 (OBF INCI辞書を使用)、(d) LLMによる正規化 (Claudeにクエリ: "この表記は、どのINCI成分を指しますか?")。 出典: Open Beauty Facts GitHub — INCI辞書の問題 [6]。
2. **CAS → SMILESのマッピングが見つからない場合 (特に天然成分の場合)** 症状: 植物エキス、発酵生成物、高級エッセンシャルオイルなど、単一のCAS番号またはSMILESがPubChemに存在しない。 原因: 天然成分は、多くの場合、複数の化合物で構成されています。単一のSMILESは適切ではありません。 緩和策: (a) 天然成分を別のカテゴリに分離する、(b) MACCSキーをテキスト埋め込みに置き換える (LLMを使用して成分の説明を埋め込む)、(c) 代表的な活性成分のみをフィンガープリントする (例: Centella Asiatica → マデカッソシド)。 出典: PubChem "天然製品" ドキュメント [8]。
3. **過度に慎重な競合ルールと、個々の違いを無視すること** 症状: システムは、レチノール + ナイアシンアミドの競合警告を自動的に表示するが、最近の研究では、この組み合わせは実際には問題ないことが示唆されている。これにより、ユーザーの混乱が生じる。 原因: 化粧品化学の研究は常に更新されており、皮膚科学界内には異なる意見がある。 緩和策: (a) ルールの信頼性 ("確実" / "議論の余地がある") を表示する、(b) 参照先のURLを提供する、(c) ユーザーがUIを通じてルールを却下できるようにする、(d) ルールセットを定期的に更新する (例: 四半期ごとのレビュー)、(e) "情報"の重大度レベルをUIでデフォルトで折りたたむように設定する。 出典: Journal of Cosmetic Dermatology、ナイアシンアミドとビタミンCに関する議論のレビュー [12]。
4. **顔認識CNNにおけるドメインシフト (照明、民族性、角度)** 症状: 自然画像でトレーニングされたCNNは、ユーザーのセルフィー (スマートフォンによるさまざまな照明と角度) でパフォーマンスが低下する。 原因: トレーニングデータの照明と人口統計の多様性が不十分。データセットは、特定の民族性(例:ISICは白人肌に偏っている)に偏っている。 緩和策: (a) 多様な民族性と照明データでファインチューニングする、(b) UIを提供して、照明と角度についてユーザーにガイダンスを提供する、(c) 結果の信頼度を表示する、(d) 他の側面(コアロジックは、成分の埋め込みとルールエンジンに焦点を当てる)を優先する。 出典: Wen et al. "Characteristics of publicly available skin cancer image datasets: a systematic review." Lancet Digital Health 2022 [13]。
5. **個人データ保護に関する規制 (韓国個人情報保護法、GDPR、CCPA)** 症状: 顔画像、アレルギープロファイル、または妊娠状態を保存すると、規制に違反する可能性がある。 原因: 顔画像は、韓国個人情報保護法およびGDPR第9条の特別なカテゴリの下で、生体データと見なされる。妊娠状態は、健康情報と見なされる (GDPR特別なカテゴリ)。 緩和策: (a) 顔画像をローカルでブラウザでのみ処理する (サーバーへの送信は行わない)、(b) ユーザープロファイルをセッション内にのみ保存し、データベースに保存する場合はデータを暗号化し、明示的な同意を得る、(c) プロファイリングと広告は使用しないことを明確に述べる、(d) プライバシーポリシーと同意フォームのための必須のUI要素を提供する、(e) 14歳未満の子供の使用を制限する。 出典: 化粧品および美容アプリに関する個人情報保護委員会のガイドライン。GDPR第9条および第22条。
## 拡張アイデア
- **バーコードスキャンアプリとの連携:** ユーザーが店頭でバーコードをスキャンすることで、成分情報、適合性、警告に瞬時にアクセスできるようにします。- **多言語ラベルOCR:** 異なる言語の化粧品ラベルの画像をキャプチャし、Tesseract OCRを使用して、INCI(国際化粧品成分名)を自動的に解析します。- **アレルギープロファイルの拡張:** ユーザーがアレルギーを引き起こす可能性のある物質を登録できるようにし、それらの物質を含む製品を瞬時にフィルタリングし、類似の成分についても警告できるようにします(フィンガープリントの類似性に基づいて)。- **環境サステナビリティフィルター:** 製品にマイクロプラスチック、パーム油、動物実験の有無に関するタグを付けます。- **価格と成分密度のスコア:** 活性成分の位置と価格を相関させます。- **コミュニティレビューのセンチメント分析:** Open Beauty FactsのレビューやRedditのr/SkincareAddiction(ライセンスに注意)からデータを収集し、LLM(大規模言語モデル)を使用してセンチメント分析を行います。- **韓国固有のKFDA(韓国食品医薬品安全処)承認ステータスの表示:** シワの軽減、美白、紫外線防御のために認証された機能性化粧品にタグを付けます。
## 次のセクション
- セクション07 `drug-target-gnn`: GNN(グラフニューラルネットワーク)を使用して、成分と皮膚受容体の相互作用を定量化します。- セクション09 `llm-vendor-benchmark`: このセクションの自然言語による推奨を、Claude、GPT、Geminiを使用してベンチマークします。- セクション14 `bio-mcp-agent`: このパイプラインをMCP(機械学習パイプライン)ツールとして公開し、チャットボットスタイルの美容アドバイザーを作成します。
## 参考文献
1. EU規則(EC)No 1223/2009(化粧品規則):`https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02009R1223`2. FDA化粧品ラベルガイド(21 CFR 701):`https://www.fda.gov/cosmetics/cosmetics-labeling`3. 大韓民国化粧品法(MFDS):`https://www.mfds.go.kr/`4. 日本の準医薬品および化粧品ラベル表示基準:`https://www.mhlw.go.jp/`5. Anthropic Claude APIの価格:`https://www.anthropic.com/pricing`6. Open Beauty Facts:`https://world.openbeautyfacts.org/` / GitHub:`https://github.com/openfoodfacts/openbeautyfacts-server`7. CosIng(EU委員会化粧品成分データベース):`https://ec.europa.eu/growth/tools-databases/cosing/`8. PubChem REST APIドキュメント:`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(国際皮膚画像コラボレーション):`https://www.isic-archive.com/`12. Journal of Cosmetic Dermatology(ナイアシンアミドとビタミンCに関する議論):様々な論文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. アメリカ皮膚科学会ガイドライン:`https://www.aad.org/`15. アメリカ産婦人科医会(ACOG)ガイドライン:`https://www.acog.org/`16. GDPR第9条(特別なカテゴリー):`https://gdpr-info.eu/art-9-gdpr/`17. RDKitフィンガープリントドキュメント:`https://www.rdkit.org/docs/GettingStartedInPython.html`18. rapidfuzz(ファジーマッチング):`https://github.com/rapidfuzz/RapidFuzz`19. INCI Beauty(コミュニティリファレンス):`https://incibeauty.com/`20. EWG Skin Deep(リファレンス、方法論については議論の余地があります):`https://www.ewg.org/skindeep/`