Spaces:
Sleeping
Sleeping
| """์ถ๋ก ์ ์ฉ ๋ชจ๋ธ ์ ์ โ v3 (๊ฐ์ฉ์ฑ ์ธ์ง ์ปจํด์ ์ ์ ์ฑ ). | |
| ์ํ = ์ฌ๊ณ (1) + ํ์ดํ๋ผ์ธ(7) + ์๊ฐ sin/cos(2) + ์์์์ธก(3) + ์ ์ฒด๊ฐ์ฉ์ฑ(5) = 18์ฐจ์. | |
| ๋งค์ผ ์ผ๋ถ ์ ์ฒด๊ฐ ํ์ ๋ ์ ์์ผ๋ฉฐ, ์ ์ฑ ์ ๊ฐ์ฉ ์ ์ฒด ์ค ์ํฉ๋ณ ์ต์ ์ ์ ํํ๋ค. | |
| ํ์ต/ํ๊ฒฝ ๋ก์ง์ train_v3.py ์ฐธ์กฐ. | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| DEVICE = torch.device("cpu") # ๋ฐฐํฌ๋ CPU ์ถ๋ก | |
| CONFIG = {"CAP": 2000.0, "tau_max": 7} | |
| TRAIN_CONFIG = {"hidden_dim": 192, "supplier_emb_dim": 48, "num_layers": 4, "num_heads": 8} | |
| SUPPLIERS = { | |
| 0: {"name": "None", "tau": 0, "P": 0, "MOQ": 0, "C_cap": 1, "C_ship": 0}, | |
| 1: {"name": "A", "tau": 7, "P": 80, "MOQ": 1000, "C_cap": 2000, "C_ship": 5000}, | |
| 2: {"name": "B", "tau": 5, "P": 100, "MOQ": 500, "C_cap": 1000, "C_ship": 3000}, | |
| 3: {"name": "C", "tau": 3, "P": 120, "MOQ": 200, "C_cap": 1000, "C_ship": 2000}, | |
| 4: {"name": "D", "tau": 1, "P": 150, "MOQ": 50, "C_cap": 500, "C_ship": 5000}, | |
| 5: {"name": "E", "tau": 0, "P": 200, "MOQ": 0, "C_cap": 100, "C_ship": 2000}, | |
| } | |
| SEAS_M = [1.0, 1.0, 0.9, 1.0, 1.1, 1.2, 1.5, 1.5, 1.0, 0.9, 1.1, 1.3] | |
| SEAS_D = [1.2, 1.1, 1.0, 1.0, 0.9, 0.6, 0.5] | |
| N_SUP = 5 # ๊ฐ์ฉ์ฑ ๋ง์คํน ๋์ (A~E) | |
| STATE_DIM = 1 + CONFIG["tau_max"] + 2 + 3 + N_SUP # = 18 | |
| def demand_factors(t): | |
| def mu(tt): | |
| return SEAS_M[(tt // 30) % 12] * SEAS_D[tt % 7] | |
| return [mu(t), sum(mu(t + k) for k in range(1, 4)) / 3, sum(mu(t + k) for k in range(1, 8)) / 7] | |
| class InventoryTransformer(nn.Module): | |
| def __init__(self, state_dim, hidden_dim, num_layers=2, num_heads=4): | |
| super().__init__() | |
| self.input_embed = nn.Linear(state_dim, hidden_dim) | |
| enc = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=num_heads, | |
| dim_feedforward=256, batch_first=True, dropout=0.0) | |
| self.transformer = nn.TransformerEncoder(enc, num_layers=num_layers) | |
| self.supplier_head = nn.Sequential(nn.Linear(hidden_dim, 64), nn.ReLU(), nn.Linear(64, 6)) | |
| self.sup_action_embed = nn.Embedding(6, TRAIN_CONFIG["supplier_emb_dim"]) | |
| self.qty_input_dim = hidden_dim + TRAIN_CONFIG["supplier_emb_dim"] | |
| self.qty_head_mu = nn.Sequential(nn.Linear(self.qty_input_dim, 64), nn.ReLU(), nn.Linear(64, 1)) | |
| self.qty_head_sigma = nn.Sequential(nn.Linear(self.qty_input_dim, 64), nn.ReLU(), | |
| nn.Linear(64, 1), nn.Softplus()) | |
| def forward(self, state, fixed_supplier=None): | |
| x = self.input_embed(state).unsqueeze(1) | |
| feats = self.transformer(x).squeeze(1) | |
| sup_logits = self.supplier_head(feats) | |
| selected = torch.argmax(sup_logits, dim=1) if fixed_supplier is None else fixed_supplier | |
| sup_emb = self.sup_action_embed(selected) | |
| qty_input = torch.cat([feats, sup_emb], dim=1) | |
| mu = F.softplus(self.qty_head_mu(qty_input)) | |
| sigma = self.qty_head_sigma(qty_input) + 1e-4 | |
| return sup_logits, mu, sigma | |
| def apply_mask(sup_logits, avail): | |
| """ํ์ ์ ์ฒด(A~E) ๋ก์ง์ -inf. None(0)์ ํญ์ ํ์ฉ.""" | |
| masked = sup_logits.clone() | |
| masked[:, 1:1 + N_SUP] = torch.where(avail > 0.5, sup_logits[:, 1:1 + N_SUP], | |
| torch.full_like(sup_logits[:, 1:1 + N_SUP], -1e9)) | |
| return masked | |
| def apply_moq(supplier_idx: int, raw_qty: float) -> int: | |
| """๋ชจ๋ธ raw ์ฃผ๋ฌธ๋ โ ์ค์ ๋ฐ์ฃผ๋ (MOQ ๋ณด์ ).""" | |
| if supplier_idx == 0: | |
| return 0 | |
| return int(round(max(raw_qty, float(SUPPLIERS[supplier_idx]["MOQ"])))) | |
| def build_state(on_hand, pipeline, day_of_year, available): | |
| """์์ ์ ๋ ฅ โ ์ํ ํ ์ (1, 18). | |
| on_hand : ํ์ฌ ๋ณด์ ์ฌ๊ณ | |
| pipeline : ํฅํ 1..7์ผ ์ ๊ณ ์์ (๊ธธ์ด 7) | |
| day_of_year : 0~364 | |
| available : A~E ๊ฐ์ฉ ์ฌ๋ถ (๊ธธ์ด 5, 1/0 ๋๋ bool) | |
| """ | |
| cap = CONFIG["CAP"]; tau_max = CONFIG["tau_max"] | |
| if len(pipeline) != tau_max: | |
| raise ValueError(f"pipeline ๊ธธ์ด {tau_max} ํ์ (๋ฐ์ {len(pipeline)})") | |
| if len(available) != N_SUP: | |
| raise ValueError(f"available ๊ธธ์ด {N_SUP} ํ์ (๋ฐ์ {len(available)})") | |
| sin_t = math.sin(2 * math.pi * day_of_year / 365) | |
| cos_t = math.cos(2 * math.pi * day_of_year / 365) | |
| feat = ([on_hand / cap] + [p / cap for p in pipeline] + [sin_t, cos_t] | |
| + demand_factors(day_of_year) + [float(a) for a in available]) | |
| return torch.tensor([feat], dtype=torch.float32, device=DEVICE) | |
| def recommend(model, on_hand, pipeline, day_of_year, available): | |
| """๊ฐ์ฉ์ฑ ๋ง์คํน + ๊ฒฐ์ ์ ์ ํ + MOQ ๋ณด์ . ๋ฐํ: (idx, name, qty).""" | |
| state = build_state(on_hand, pipeline, day_of_year, available) | |
| sup_logits, _, _ = model(state) | |
| masked = apply_mask(sup_logits, state[:, -N_SUP:]) | |
| idx = int(torch.argmax(masked, dim=1).item()) | |
| _, mu, _ = model(state, fixed_supplier=torch.tensor([idx], device=DEVICE)) | |
| qty = apply_moq(idx, mu.item()) | |
| return idx, SUPPLIERS[idx]["name"], qty | |
| def load_model(model_path: str) -> InventoryTransformer: | |
| model = InventoryTransformer(STATE_DIM, TRAIN_CONFIG["hidden_dim"], | |
| TRAIN_CONFIG["num_layers"], TRAIN_CONFIG["num_heads"]).to(DEVICE) | |
| checkpoint = torch.load(model_path, map_location=DEVICE, weights_only=True) | |
| model.load_state_dict(checkpoint) | |
| model.eval() | |
| return model | |