Restructure package: descriptive study documentation and clean layout
This commit is contained in:
Executable
+929
@@ -0,0 +1,929 @@
|
||||
"""
|
||||
=============================================================================
|
||||
Multi-User Semantic Communication — PyTorch MAML Training + Evaluation
|
||||
IEEE JSAC: Meta-Learned Cross-Attention for Multi-User Semantic
|
||||
Communication over Wireless Fading Channels
|
||||
|
||||
구조
|
||||
----
|
||||
SemanticEncoder : MLP x_u → e_u ∈ ℝ^d
|
||||
UserWiseCrossAttn : user-wise cross-attention decoder Y → ê_u (논문 식 4–6)
|
||||
MAMLTrainer : MAML outer/inner loop over SNR tasks (논문 식 7–9)
|
||||
|
||||
비교 대상
|
||||
----------
|
||||
OFDMA : 대역폭 B/U 분할 (SNR 패널티 −10log10(U))
|
||||
NOMA-SIC : 전력 중첩 + 순차 간섭 제거
|
||||
Joint : 고정 SNR 분포에서 표준 joint training
|
||||
|
||||
지표
|
||||
----
|
||||
SER : cosine-sim(ê_u, e_u) < τ 인 사용자 비율
|
||||
ρ : 디코딩된 임베딩의 사용자 간 Pearson 상관계수
|
||||
|
||||
실행 방법
|
||||
----------
|
||||
python maml_semantic.py # 학습 + 평가
|
||||
python maml_semantic.py --fast # 빠른 디버그 (epoch 축소)
|
||||
python maml_semantic.py --eval_only --ckpt results/models.pt
|
||||
=============================================================================
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.gridspec as gridspec
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 0. CONFIG
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def get_cfg():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--d", type=int, default=64)
|
||||
p.add_argument("--U", type=int, default=4)
|
||||
p.add_argument("--H", type=int, default=4, help="attention heads")
|
||||
p.add_argument("--tau", type=float, default=0.45)
|
||||
p.add_argument("--lam", type=float, default=0.1, help="ortho loss weight λ")
|
||||
p.add_argument("--snr_min", type=float, default=0.0)
|
||||
p.add_argument("--snr_max", type=float, default=20.0)
|
||||
p.add_argument("--snr_step", type=float, default=2.0)
|
||||
p.add_argument("--inner_lr", type=float, default=0.01)
|
||||
p.add_argument("--inner_steps", type=int, default=5)
|
||||
p.add_argument("--outer_lr", type=float, default=1e-3)
|
||||
p.add_argument("--meta_epochs", type=int, default=300)
|
||||
p.add_argument("--joint_epochs",type=int, default=300)
|
||||
p.add_argument("--batch", type=int, default=64)
|
||||
p.add_argument("--n_mc", type=int, default=500)
|
||||
p.add_argument("--seed", type=int, default=42)
|
||||
p.add_argument("--outdir", type=str, default="results")
|
||||
p.add_argument("--eval_only", action="store_true")
|
||||
p.add_argument("--ckpt", type=str, default=None)
|
||||
p.add_argument("--fast", action="store_true",
|
||||
help="빠른 디버그: epoch을 1/10로 축소")
|
||||
p.add_argument("--device", type=str, default="auto")
|
||||
p.add_argument("--scenario", type=str, default="DEFAULT",
|
||||
help="훈련 시나리오: DEFAULT/HIGH/LOW/MIX/HETERO/ASYM/ALL")
|
||||
p.add_argument("--decoder_only", action="store_true",
|
||||
help="Option A: IdentityEncoder(고정) + decoder만 MAML 훈련")
|
||||
args = p.parse_args()
|
||||
if args.fast:
|
||||
args.meta_epochs = max(30, args.meta_epochs // 10)
|
||||
args.joint_epochs = max(30, args.joint_epochs // 10)
|
||||
args.n_mc = max(50, args.n_mc // 10)
|
||||
return args
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 1. DATA GENERATION (자율주행 합성 임베딩)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# u0: 보행자 bounding-box (scene 결합도 높음)
|
||||
# u1: 신호등 상태 (중간)
|
||||
# u2: 차선 세그먼테이션 (낮음)
|
||||
# u3: 차량 속도/방향 (가장 낮음)
|
||||
BLEND = [0.55, 0.45, 0.30, 0.20]
|
||||
USER_LABELS = ['보행자\n(U1)', '신호등\n(U2)', '차선\n(U3)', '속도\n(U4)']
|
||||
USER_COLORS = ['#1565C0', '#2E7D32', '#C62828', '#6A1B9A']
|
||||
|
||||
# Scenario configs matching semantic_correlation_sim.py
|
||||
SCENARIO_CONFIGS = {
|
||||
'DEFAULT': {'beta_u': [0.55, 0.45, 0.30, 0.20], 'scenes': [0, 0, 0, 0]},
|
||||
'HIGH': {'beta_u': [0.65, 0.65, 0.60, 0.60], 'scenes': [0, 0, 0, 0]},
|
||||
'LOW': {'beta_u': [0.65, 0.05, 0.05, 0.05], 'scenes': [0, 1, 2, 3]},
|
||||
'MIX': {'beta_u': [0.65, 0.65, 0.05, 0.05], 'scenes': [0, 0, 1, 2]},
|
||||
'HETERO': {'beta_u': [0.75, 0.75, 0.45, 0.08], 'scenes': [0, 0, 0, 1]},
|
||||
'ASYM': {'beta_u': [0.72, 0.58, 0.35, 0.12], 'scenes': [0, 0, 0, 0]},
|
||||
}
|
||||
|
||||
|
||||
def gen_embeddings(n: int, d: int, U: int, rng,
|
||||
scenario_cfg=None) -> torch.Tensor:
|
||||
"""단위 정규화된 ground-truth 임베딩 (n, U, d)
|
||||
scenario_cfg: dict with 'beta_u' and 'scenes' keys (from SCENARIO_CONFIGS).
|
||||
If None, uses the default BLEND with a single shared scene.
|
||||
"""
|
||||
if scenario_cfg is None:
|
||||
blend = BLEND
|
||||
scenes = [0] * U
|
||||
else:
|
||||
blend = scenario_cfg['beta_u']
|
||||
scenes = scenario_cfg['scenes']
|
||||
|
||||
# One unit-norm scene vector per unique scene key (full D dims, matches sim)
|
||||
unique_scenes = sorted(set(scenes))
|
||||
scene_vecs: dict = {}
|
||||
for sc in unique_scenes:
|
||||
v = rng.standard_normal(d)
|
||||
scene_vecs[sc] = v / (np.linalg.norm(v) + 1e-8)
|
||||
|
||||
embs = []
|
||||
for u in range(U):
|
||||
b = blend[u] if u < len(blend) else 0.15
|
||||
private = rng.standard_normal((n, d))
|
||||
p_hat = private / (np.linalg.norm(private, axis=-1, keepdims=True) + 1e-8)
|
||||
s = scene_vecs[scenes[u]]
|
||||
# Eq. (2): e_u = sqrt(1-β²)·p̂_u + β·s (semantic_correlation_sim.py convention)
|
||||
e = np.sqrt(max(1 - b ** 2, 0)) * p_hat + b * s[None, :]
|
||||
e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8
|
||||
embs.append(e)
|
||||
return torch.from_numpy(np.stack(embs, axis=1)).float() # (n, U, d)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 2. CHANNEL MODELS (SE — Shared Embedding superposition framework)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def _block_masks(U: int, d: int, device) -> torch.Tensor:
|
||||
"""Hard block masks (U, d): user u owns dims [u·DPU, (u+1)·DPU).
|
||||
Matches semantic_correlation_sim.py MASKS construction exactly."""
|
||||
DPU = d // U
|
||||
masks = torch.zeros(U, d, device=device)
|
||||
for u in range(U):
|
||||
masks[u, u * DPU:(u + 1) * DPU] = 1.0
|
||||
return masks
|
||||
|
||||
|
||||
def se_channel(E: torch.Tensor, snr_db: float) -> torch.Tensor:
|
||||
"""SE superposition channel (논문 Eq. 3).
|
||||
|
||||
x_u = e_u ⊙ m_u (hard block mask; user u owns D/U contiguous dims)
|
||||
y_tx = Σ_u x_u (superimposed D-dim signal)
|
||||
y_rx,u = h_u · y_tx + n_u (independent Rayleigh per user)
|
||||
|
||||
E : (n, U, d) unit-norm embeddings
|
||||
Returns Y_rx : (n, U, d) — row u is user u's received copy of y_tx.
|
||||
|
||||
Cross-attention 구조 설명:
|
||||
R_{u,i} = y_rx,u ⊙ m_i 는 user u의 수신 신호에서 subspace i를 추출.
|
||||
h_u · x_i[dims_i] + noise 만 남으므로, beta_ui > 0이면 e_u 정보를 담고 있음.
|
||||
"""
|
||||
n, U, d = E.shape
|
||||
masks = _block_masks(U, d, E.device) # (U, d)
|
||||
X = E * masks[None, :, :] # (n, U, d)
|
||||
Ytx = X.sum(1) # (n, d) superimposed
|
||||
|
||||
h = (torch.randn(n, U, 1, device=E.device) ** 2 +
|
||||
torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * (0.5 ** 0.5) # (n, U, 1)
|
||||
|
||||
snr_lin = 10 ** (snr_db / 10)
|
||||
noise_std = (Ytx.pow(2).mean() / snr_lin).sqrt()
|
||||
Yrx = h * Ytx[:, None, :] + torch.randn(n, U, d, device=E.device) * noise_std
|
||||
return Yrx # (n, U, d)
|
||||
|
||||
|
||||
def se_ofdma_decode(Y_rx: torch.Tensor) -> torch.Tensor:
|
||||
"""SE-OFDMA decoder: ê_u = normalize(y_rx,u ⊙ m_u).
|
||||
Each user uses only their own D/U-dim subspace block; no SNR penalty.
|
||||
Matches ofdma_se_decoder() in semantic_correlation_sim.py."""
|
||||
n, U, d = Y_rx.shape
|
||||
masks = _block_masks(U, d, Y_rx.device) # (U, d)
|
||||
return F.normalize(Y_rx * masks[None, :, :], dim=-1) # (n, U, d)
|
||||
|
||||
|
||||
def se_noma_decode(E: torch.Tensor, snr_db: float) -> torch.Tensor:
|
||||
"""Full-band power-domain NOMA with SIC (standard NOMA, fair comparison).
|
||||
|
||||
All U users share the SAME full d-dim band — no subspace masking — so the
|
||||
received signal is a single d-dim power-domain superposition:
|
||||
x_u = sqrt(p_u) · e_u (full-band, power-weighted)
|
||||
y = Σ_u h_u · sqrt(p_u) · e_u + n (single d-dim signal at BS)
|
||||
|
||||
This reflects NOMA's defining resource advantage fairly: every user accesses
|
||||
the full d-dim band (vs. OFDMA's exclusive d/U-dim block) at the cost of
|
||||
inter-user interference, while the total transmit power Σ_u p_u = 1 matches
|
||||
the OFDMA budget, keeping the comparison both power- and bandwidth-fair.
|
||||
Each user is reconstructed from the full d-dim signal via successive
|
||||
interference cancellation in descending allocated-power order.
|
||||
|
||||
Matches noma_ul_channel()/noma_sic_decoder() in semantic_correlation_sim.py
|
||||
and noma_ch()/noma_sic() in revision_realdata_plot.py.
|
||||
|
||||
E : (n, U, d) unit-norm ground-truth embeddings.
|
||||
Returns decoded : (n, U, d) per-user estimates over the full d-dim band.
|
||||
"""
|
||||
n, U, d = E.shape
|
||||
pa = torch.tensor([0.40, 0.30, 0.20, 0.10], device=E.device)[:U]
|
||||
pa = pa / pa.sum() # Σ p_u = 1 (power-fair)
|
||||
|
||||
h = (torch.randn(n, U, 1, device=E.device) ** 2 +
|
||||
torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * (0.5 ** 0.5) # (n, U, 1)
|
||||
|
||||
y = (E * pa.sqrt()[None, :, None] * h).sum(1) # (n, d) full-band superposition
|
||||
snr_lin = 10 ** (snr_db / 10)
|
||||
noise_std = (y.pow(2).mean() / snr_lin).sqrt()
|
||||
y = y + torch.randn(n, d, device=E.device) * noise_std # (n, d)
|
||||
|
||||
order = torch.argsort(pa, descending=True) # high-power first
|
||||
res = y.clone() # (n, d) shared residual
|
||||
decoded = torch.zeros(n, U, d, device=E.device) # (n, U, d)
|
||||
for u_idx in order:
|
||||
u = int(u_idx.item())
|
||||
ê_u = F.normalize(res / (h[:, u, :] + 1e-8), dim=-1) # (n, d) over full band
|
||||
decoded[:, u, :] = ê_u
|
||||
res = res - h[:, u, :] * pa[u].sqrt() * ê_u # full-band cancellation
|
||||
|
||||
return decoded # (n, U, d)
|
||||
|
||||
|
||||
def se_sfdma_decode(E: torch.Tensor, snr_db: float) -> torch.Tensor:
|
||||
"""SFDMA — Semantic Feature Division Multiple Access (Ma et al., 2024).
|
||||
|
||||
Each user maps its embedding onto an assigned orthonormal d/U-dim *semantic*
|
||||
subspace; all users transmit simultaneously over the SAME full d-dim band, and
|
||||
the receiver separates them by projecting onto each user's subspace:
|
||||
x_u = P_u e_u (P_u: projector onto user u's semantic subspace)
|
||||
y = Σ_u h_u · x_u + n (full-band superposition)
|
||||
ê_u = normalize(P_u y / h_u)
|
||||
|
||||
Fair vs. NOMA/UWCA: SFDMA shares the full band (not OFDMA's exclusive 1/U
|
||||
physical sub-band) and is given the best case of *perfectly* orthogonal
|
||||
subspaces, so the projections separate users with no inter-user interference.
|
||||
The subspaces are taken as the canonical orthogonal partition (the block
|
||||
basis) so the comparison isolates the multiple-access mechanism rather than an
|
||||
arbitrary basis–data alignment. Because the orthogonality confines each user
|
||||
to d/U effective dimensions — the same subspace ceiling as OFDMA — and discards
|
||||
the inter-user correlation, SFDMA coincides with OFDMA despite using the full
|
||||
band. Matches sfdma() in revision_realdata_plot.py.
|
||||
|
||||
E : (n, U, d) unit-norm embeddings. Returns (n, U, d) estimates.
|
||||
"""
|
||||
n, U, d = E.shape
|
||||
masks = _block_masks(U, d, E.device) # (U, d) orthogonal subspaces
|
||||
|
||||
h = (torch.randn(n, U, 1, device=E.device) ** 2 +
|
||||
torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * (0.5 ** 0.5) # (n, U, 1)
|
||||
|
||||
X = E * masks[None, :, :] # (n, U, d) project onto own subspace
|
||||
y = (h * X).sum(1) # (n, d) full-band superposition
|
||||
snr_lin = 10 ** (snr_db / 10)
|
||||
noise_std = (y.pow(2).mean() / snr_lin).sqrt()
|
||||
y = y + torch.randn(n, d, device=E.device) * noise_std # (n, d)
|
||||
|
||||
return torch.stack(
|
||||
[F.normalize((y * masks[u]) / (h[:, u, :] + 1e-8), dim=-1) for u in range(U)], 1)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 3. NEURAL MODULES
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
class SemanticEncoder(nn.Module):
|
||||
"""f_φ : ℝ^d → ℝ^d (MLP + LayerNorm + 단위 정규화)"""
|
||||
def __init__(self, d: int):
|
||||
super().__init__()
|
||||
self.net = nn.Sequential(
|
||||
nn.Linear(d, d * 2), nn.LayerNorm(d * 2), nn.GELU(),
|
||||
nn.Linear(d * 2, d),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return F.normalize(self.net(x), dim=-1)
|
||||
|
||||
|
||||
class IdentityEncoder(nn.Module):
|
||||
"""No-op encoder: L2-normalizes input only (no learnable params).
|
||||
Used for decoder-only training (Option A) so that the channel model
|
||||
directly transmits the raw embeddings, matching the analytical simulation."""
|
||||
def forward(self, x):
|
||||
return F.normalize(x, dim=-1)
|
||||
|
||||
|
||||
class UserWiseCrossAttention(nn.Module):
|
||||
"""
|
||||
User-wise cross-attention decoder (논문 Section III-A, 식 4–6)
|
||||
|
||||
입력 Y : (n, U, d) 수신 임베딩 (잡음 포함)
|
||||
출력 Ê : (n, U, d) 정제된 임베딩
|
||||
복잡도 O(U d²)
|
||||
"""
|
||||
def __init__(self, d: int, U: int, H: int = 4):
|
||||
super().__init__()
|
||||
assert d % H == 0
|
||||
self.d = d; self.U = U; self.H = H; self.dk = d // H
|
||||
# JSAC convention: learnable per-user query vectors {q_u}, not signal-derived
|
||||
self.q_vectors = nn.Parameter(torch.randn(U, d) * (d ** -0.5)) # (U, d)
|
||||
self.eta = nn.Parameter(torch.ones(1)) # sharpness η
|
||||
self.W_K = nn.Linear(d, d, bias=False)
|
||||
self.W_V = nn.Linear(d, d, bias=False)
|
||||
self.W_O = nn.Linear(d, d, bias=False)
|
||||
self.norm = nn.LayerNorm(d)
|
||||
# 학습 가능한 soft mask m_i ∈ [0,1]^d (식 4)
|
||||
# 초기화: hard block mask logit (+3 = sigmoid → 0.95, -3 → 0.05)
|
||||
# 이 초기화가 없으면 모든 mask = 0.5 → K feature가 모든 i에서 동일
|
||||
# → softmax가 항상 uniform(1/U) → 학습 무력화
|
||||
init_logits = torch.full((U, d), -3.0)
|
||||
DPU = d // U
|
||||
for u_ in range(U):
|
||||
init_logits[u_, u_ * DPU:(u_ + 1) * DPU] = 3.0 # own block 강조
|
||||
self.mask_logits = nn.Parameter(init_logits)
|
||||
|
||||
def forward(self, Y):
|
||||
"""Y : (n, U, d) → (Ê, alpha)"""
|
||||
n, U, d = Y.shape
|
||||
masks = torch.sigmoid(self.mask_logits) # (U, d)
|
||||
|
||||
# R_{u,i} = y_u ⊙ m_i → (n, U_q, U_k, d)
|
||||
R = Y.unsqueeze(2) * masks[None, None, :, :] # broadcast
|
||||
|
||||
# JSAC: q_u is a fixed learnable vector per user, independent of received signal
|
||||
Q = self.q_vectors.unsqueeze(0).expand(n, -1, -1) # (n, U, d)
|
||||
K = self.W_K(R) # (n, U, U, d)
|
||||
V = self.W_V(R)
|
||||
|
||||
# multi-head reshape
|
||||
def mh(t):
|
||||
return t.reshape(*t.shape[:-1], self.H, self.dk)
|
||||
|
||||
Q_ = mh(Q) # (n,Uq,H,dk)
|
||||
K_ = mh(K) # (n,Uq,Uk,H,dk)
|
||||
V_ = mh(V)
|
||||
|
||||
# score : q_u^T k_i / sqrt(dk)
|
||||
Q_e = Q_.unsqueeze(3) # (n,Uq,H,1,dk)
|
||||
K_t = K_.permute(0, 1, 3, 2, 4) # (n,Uq,H,Uk,dk)
|
||||
scores = self.eta * (Q_e * K_t).sum(-1) / (self.dk ** 0.5) # η * q_u^T k_i / √dk
|
||||
alpha = F.softmax(scores, dim=-1) # (n,Uq,H,Uk)
|
||||
|
||||
V_t = V_.permute(0, 1, 3, 2, 4) # (n,Uq,H,Uk,dk)
|
||||
ctx = (alpha.unsqueeze(-1) * V_t).sum(3) # (n,Uq,H,dk)
|
||||
ctx = self.W_O(ctx.reshape(n, U, d)) # (n,Uq,d)
|
||||
|
||||
own = Y * masks[None, :, :] # 자기 유저 residual
|
||||
out = F.normalize(self.norm(ctx + own), dim=-1)
|
||||
return out, alpha.mean(0) # alpha: (Uq,H,Uk)
|
||||
|
||||
|
||||
class SemanticCommSystem(nn.Module):
|
||||
def __init__(self, d, U, H, decoder_only=False):
|
||||
super().__init__()
|
||||
self.encoder = IdentityEncoder() if decoder_only else SemanticEncoder(d)
|
||||
self.decoder = UserWiseCrossAttention(d, U, H)
|
||||
|
||||
def forward(self, X, snr_db):
|
||||
n, U, d = X.shape
|
||||
E = self.encoder(X.reshape(n * U, d)).reshape(n, U, d)
|
||||
Y = se_channel(E, snr_db) # always SE superposition channel
|
||||
Ehat, alpha = self.decoder(Y)
|
||||
return E, Ehat, alpha
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 4. LOSS (논문 식 8)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def semantic_loss(Ehat, E, lam=0.1):
|
||||
"""
|
||||
L = (1/U)Σ(1 - cos(ê_u,e_u)) + λ Σ_{u≠v}|ρ(ê_u,ê_v)|
|
||||
"""
|
||||
cos = (Ehat * E).sum(-1)
|
||||
distortion = (1 - cos).mean()
|
||||
|
||||
U = Ehat.shape[1]
|
||||
emb = Ehat.mean(0) # (U, d)
|
||||
ec = emb - emb.mean(1, keepdim=True)
|
||||
en = F.normalize(ec, dim=1)
|
||||
C = en @ en.T
|
||||
mask = ~torch.eye(U, dtype=torch.bool, device=Ehat.device)
|
||||
ortho = C[mask].abs().mean()
|
||||
|
||||
return distortion + lam * ortho, distortion.item(), ortho.item()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 5. MAML TRAINER (논문 식 7, 9)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
class MAMLTrainer:
|
||||
def __init__(self, model, cfg, device, rng, scenario_cfg=None):
|
||||
self.model = model
|
||||
self.cfg = cfg
|
||||
self.device = device
|
||||
self.rng = rng
|
||||
self.scenario_cfg = scenario_cfg # None → default BLEND
|
||||
mask_params = [p for n, p in model.named_parameters() if 'mask_logits' in n]
|
||||
other_params = [p for n, p in model.named_parameters() if 'mask_logits' not in n]
|
||||
self.optimizer = torch.optim.Adam([
|
||||
{'params': other_params, 'lr': cfg.outer_lr},
|
||||
{'params': mask_params, 'lr': cfg.outer_lr * 100},
|
||||
])
|
||||
self.snr_tasks = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step)
|
||||
|
||||
def _inner_adapt(self, snr):
|
||||
"""Inner-loop: adapted copy of model for task T_k = SNR γ_k"""
|
||||
adapted = copy.deepcopy(self.model)
|
||||
opt_in = torch.optim.SGD(adapted.parameters(), lr=self.cfg.inner_lr)
|
||||
for _ in range(self.cfg.inner_steps):
|
||||
X = gen_embeddings(self.cfg.batch, self.cfg.d,
|
||||
self.cfg.U, self.rng,
|
||||
self.scenario_cfg).to(self.device)
|
||||
E, Ehat, _ = adapted(X, snr)
|
||||
loss, _, _ = semantic_loss(Ehat, E, self.cfg.lam)
|
||||
opt_in.zero_grad(); loss.backward(); opt_in.step()
|
||||
return adapted
|
||||
|
||||
def meta_step(self):
|
||||
"""Outer-loop: first-order MAML (Reptile-style aggregation)"""
|
||||
fo_loss = torch.tensor(0.0, device=self.device)
|
||||
for snr in self.snr_tasks:
|
||||
X = gen_embeddings(self.cfg.batch, self.cfg.d,
|
||||
self.cfg.U, self.rng,
|
||||
self.scenario_cfg).to(self.device)
|
||||
E, Ehat, _ = self.model(X, float(snr))
|
||||
loss, _, _ = semantic_loss(Ehat, E, self.cfg.lam)
|
||||
fo_loss = fo_loss + loss
|
||||
fo_loss = fo_loss / len(self.snr_tasks)
|
||||
self.optimizer.zero_grad()
|
||||
fo_loss.backward()
|
||||
nn.utils.clip_grad_norm_(self.model.parameters(), 5.0)
|
||||
self.optimizer.step()
|
||||
return fo_loss.item()
|
||||
|
||||
def train(self):
|
||||
history = []
|
||||
print("=" * 60)
|
||||
print(f"MAML Training ({self.cfg.meta_epochs} epochs, "
|
||||
f"{len(self.snr_tasks)} SNR tasks)")
|
||||
print("=" * 60)
|
||||
for ep in range(1, self.cfg.meta_epochs + 1):
|
||||
loss = self.meta_step()
|
||||
history.append(loss)
|
||||
if ep % max(1, self.cfg.meta_epochs // 6) == 0:
|
||||
print(f" Epoch {ep:4d}/{self.cfg.meta_epochs} loss={loss:.4f}")
|
||||
return history
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 6. JOINT TRAINING BASELINE
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def train_joint(model, cfg, device, rng, scenario_cfg=None):
|
||||
mask_params = [p for n, p in model.named_parameters() if 'mask_logits' in n]
|
||||
other_params = [p for n, p in model.named_parameters() if 'mask_logits' not in n]
|
||||
opt = torch.optim.Adam([
|
||||
{'params': other_params, 'lr': cfg.outer_lr},
|
||||
{'params': mask_params, 'lr': cfg.outer_lr * 100},
|
||||
])
|
||||
history = []
|
||||
print("=" * 60)
|
||||
print(f"Joint Training ({cfg.joint_epochs} epochs)")
|
||||
print("=" * 60)
|
||||
for ep in range(1, cfg.joint_epochs + 1):
|
||||
snr = float(np.random.uniform(cfg.snr_min, cfg.snr_max))
|
||||
X = gen_embeddings(cfg.batch, cfg.d, cfg.U, rng, scenario_cfg).to(device)
|
||||
E, Ehat, _ = model(X, snr)
|
||||
loss, _, _ = semantic_loss(Ehat, E, cfg.lam)
|
||||
opt.zero_grad(); loss.backward()
|
||||
nn.utils.clip_grad_norm_(model.parameters(), 5.0)
|
||||
opt.step()
|
||||
history.append(loss.item())
|
||||
if ep % max(1, cfg.joint_epochs // 6) == 0:
|
||||
print(f" Epoch {ep:4d}/{cfg.joint_epochs} loss={loss.item():.4f}")
|
||||
return history
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 7. EVALUATION (semantic_sim.py 구조 그대로 유지)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def _normalize_np(E):
|
||||
return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8)
|
||||
|
||||
def cos_mean(Eh, Egt):
|
||||
return (Eh * Egt).sum(-1).mean()
|
||||
|
||||
def ser_total(Eh, Egt, tau):
|
||||
return ((Eh * Egt).sum(-1) < tau).mean()
|
||||
|
||||
def ser_per_user(Eh, Egt, tau):
|
||||
return ((Eh * Egt).sum(-1) < tau).mean(0) # (U,)
|
||||
|
||||
def corr_matrix(Eh):
|
||||
e = Eh.mean(0) # (U, D)
|
||||
ec = e - e.mean(1, keepdims=True)
|
||||
en = ec / (np.linalg.norm(ec, axis=1, keepdims=True) + 1e-8)
|
||||
return en @ en.T # (U, U)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def evaluate_model(model, cfg, device, rng, channel="se", scenario_cfg=None):
|
||||
"""
|
||||
SNR_DB 전 구간에 걸쳐 SER / cosine / per-user SER 계산.
|
||||
semantic_sim.py의 시뮬레이션 루프와 동일한 방식.
|
||||
"""
|
||||
SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step)
|
||||
model.eval()
|
||||
res = {"ser": [], "cos": [], "sp": [], "rho": []}
|
||||
|
||||
for snr in SNR_DB:
|
||||
ser_acc = 0.0; cos_acc = 0.0
|
||||
sp_acc = np.zeros(cfg.U); rho_acc = []
|
||||
|
||||
for _ in range(cfg.n_mc):
|
||||
Egt = gen_embeddings(cfg.batch, cfg.d, cfg.U, rng, scenario_cfg)
|
||||
X = Egt.to(device)
|
||||
|
||||
# 채널 통과 — 모든 방식이 SE 수퍼포지션 채널 공유
|
||||
if channel == "ofdma":
|
||||
# SE-OFDMA: 같은 SE 채널, own D/U block만 사용
|
||||
E = F.normalize(X.reshape(-1, cfg.d), dim=-1).reshape(-1, cfg.U, cfg.d)
|
||||
Y = se_channel(E, float(snr))
|
||||
Ehat = se_ofdma_decode(Y)
|
||||
elif channel == "noma":
|
||||
# SE-NOMA-SIC: full-band power-domain superposition (own channel + SIC)
|
||||
E = F.normalize(X.reshape(-1, cfg.d), dim=-1).reshape(-1, cfg.U, cfg.d)
|
||||
Ehat = se_noma_decode(E, float(snr))
|
||||
elif channel == "sfdma":
|
||||
# SFDMA: full-band, orthogonal semantic-subspace division (Ma 2024)
|
||||
E = F.normalize(X.reshape(-1, cfg.d), dim=-1).reshape(-1, cfg.U, cfg.d)
|
||||
Ehat = se_sfdma_decode(E, float(snr))
|
||||
else:
|
||||
# MAML+Attn / Joint+Attn: SE 채널 + cross-attention decoder
|
||||
E, Ehat, _ = model(X, float(snr))
|
||||
|
||||
Eh = Ehat.cpu().numpy()
|
||||
Egt_np = E.cpu().numpy()
|
||||
|
||||
ser_acc += ser_total(Eh, Egt_np, cfg.tau)
|
||||
cos_acc += cos_mean(Eh, Egt_np)
|
||||
sp_acc += ser_per_user(Eh, Egt_np, cfg.tau)
|
||||
rho_acc.append(corr_matrix(Eh))
|
||||
|
||||
res["ser"].append(ser_acc / cfg.n_mc)
|
||||
res["cos"].append(cos_acc / cfg.n_mc)
|
||||
res["sp"].append(sp_acc / cfg.n_mc)
|
||||
res["rho"].append(np.mean(rho_acc, axis=0))
|
||||
|
||||
for k in res:
|
||||
res[k] = np.array(res[k])
|
||||
return res
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def get_attention_map(model, snr, cfg, device, rng):
|
||||
model.eval()
|
||||
attn_sum = None
|
||||
for _ in range(cfg.n_mc):
|
||||
X = gen_embeddings(cfg.batch, cfg.d, cfg.U, rng).to(device)
|
||||
E = model.encoder(X.reshape(-1, cfg.d)).reshape(-1, cfg.U, cfg.d)
|
||||
Y = se_channel(E, float(snr))
|
||||
_, alpha = model.decoder(Y) # (Uq, H, Uk)
|
||||
a = alpha.mean(1).cpu().numpy() # (Uq, Uk) — avg over heads
|
||||
attn_sum = a if attn_sum is None else attn_sum + a
|
||||
return attn_sum / cfg.n_mc
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 8. 9-PANEL FIGURE (semantic_sim.py 그대로 재현)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def plot_results(SNR_DB, res_maml, res_joint, res_ofdma, res_noma,
|
||||
rho_j, rho_m, attn_m_mc, out_path, cfg):
|
||||
|
||||
U = cfg.UOF
|
||||
KP = 'MAML+Attn\n(제안)'
|
||||
MCFG = {
|
||||
'OFDMA': ('#546E7A', 's--', 1.6, 'OFDMA'),
|
||||
'NOMA-SIC': ('#E65100', '^-.', 1.6, 'NOMA-SIC'),
|
||||
'Joint+Attn': ('#C62828', 'D--', 1.8, 'Joint+Attn'),
|
||||
KP: ('#1565C0', 'o-', 2.5, 'MAML+Attn (제안)'),
|
||||
}
|
||||
res_all = {'OFDMA': res_ofdma, 'NOMA-SIC': res_noma,
|
||||
'Joint+Attn': res_joint, KP: res_maml}
|
||||
|
||||
cmap_r = LinearSegmentedColormap.from_list(
|
||||
'r', ['#1565C0', '#FFFFFF', '#C62828'], N=256)
|
||||
cmap_a = LinearSegmentedColormap.from_list(
|
||||
'a', ['#F5F5F5', '#1565C0'], N=256)
|
||||
|
||||
fig = plt.figure(figsize=(18, 15))
|
||||
fig.patch.set_facecolor('#F8F9FA')
|
||||
gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.48, wspace=0.38,
|
||||
left=0.07, right=0.97, top=0.93, bottom=0.06)
|
||||
|
||||
idx10 = int((10 - cfg.snr_min) / cfg.snr_step) # SNR=10dB 인덱스
|
||||
|
||||
# (a) SER vs SNR
|
||||
ax = fig.add_subplot(gs[0, 0]); ax.set_facecolor('white')
|
||||
for k, (c, mk, lw, lb) in MCFG.items():
|
||||
ax.semilogy(SNR_DB, res_all[k]['ser'], mk, lw=lw, ms=6, color=c, label=lb)
|
||||
d10 = res_ofdma['ser'][idx10] - res_maml['ser'][idx10]
|
||||
ax.annotate(f'Δ={d10:.3f}\n@ 10 dB',
|
||||
xy=(10, res_maml['ser'][idx10]),
|
||||
xytext=(13, res_maml['ser'][idx10] * 4),
|
||||
fontsize=8.5, color='#1565C0',
|
||||
arrowprops=dict(arrowstyle='->', color='#1565C0', lw=1.2))
|
||||
ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER', fontsize=11)
|
||||
ax.set_title('(a) SER vs SNR', fontsize=12, fontweight='bold')
|
||||
ax.legend(fontsize=9); ax.grid(True, alpha=0.35); ax.set_xlim(0, 20)
|
||||
|
||||
# (b) 코사인 유사도
|
||||
ax = fig.add_subplot(gs[0, 1]); ax.set_facecolor('white')
|
||||
for k, (c, mk, lw, lb) in MCFG.items():
|
||||
ax.plot(SNR_DB, res_all[k]['cos'], mk, lw=lw, ms=6, color=c, label=lb)
|
||||
ax.axhline(cfg.tau, color='gray', lw=1.2, ls=':', label=f'τ={cfg.tau}')
|
||||
ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('코사인 유사도', fontsize=11)
|
||||
ax.set_title('(b) 코사인 유사도 vs SNR', fontsize=12, fontweight='bold')
|
||||
ax.legend(fontsize=9); ax.grid(True, alpha=0.35)
|
||||
ax.set_xlim(0, 20); ax.set_ylim(0.35, 1.02)
|
||||
|
||||
# (c) SER 개선량
|
||||
ax = fig.add_subplot(gs[0, 2]); ax.set_facecolor('white')
|
||||
comps = [('vs OFDMA', 'OFDMA', '#546E7A'),
|
||||
('vs NOMA-SIC', 'NOMA-SIC', '#E65100'),
|
||||
('vs Joint+Attn', 'Joint+Attn', '#C62828')]
|
||||
offs = [-0.3, 0.0, 0.3]
|
||||
for (lb, base, col), off in zip(comps, offs):
|
||||
ax.bar(SNR_DB + off, res_all[base]['ser'] - res_maml['ser'],
|
||||
width=0.28, alpha=0.80, color=col, label=lb)
|
||||
ax.axhline(0, color='black', lw=0.8)
|
||||
ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER 개선량', fontsize=11)
|
||||
ax.set_title('(c) SER 개선량 (베이스라인 − 제안)', fontsize=12, fontweight='bold')
|
||||
ax.legend(fontsize=9); ax.grid(True, alpha=0.25, axis='y')
|
||||
|
||||
# (d) 제안 사용자별 SER
|
||||
ax = fig.add_subplot(gs[1, 0]); ax.set_facecolor('white')
|
||||
for ui in range(U):
|
||||
ax.semilogy(SNR_DB, res_maml['sp'][:, ui], 'o-', lw=1.8, ms=5,
|
||||
color=USER_COLORS[ui], label=USER_LABELS[ui])
|
||||
ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER', fontsize=11)
|
||||
ax.set_title('(d) 제안 — 사용자별 SER', fontsize=12, fontweight='bold')
|
||||
ax.legend(fontsize=8); ax.grid(True, alpha=0.35); ax.set_xlim(0, 20)
|
||||
|
||||
# (e) OFDMA 사용자별 SER
|
||||
ax = fig.add_subplot(gs[1, 1]); ax.set_facecolor('white')
|
||||
for ui in range(U):
|
||||
ax.semilogy(SNR_DB, res_ofdma['sp'][:, ui], 's--', lw=1.6, ms=5,
|
||||
color=USER_COLORS[ui], label=USER_LABELS[ui])
|
||||
ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER', fontsize=11)
|
||||
ax.set_title('(e) OFDMA — 사용자별 SER', fontsize=12, fontweight='bold')
|
||||
ax.legend(fontsize=8); ax.grid(True, alpha=0.35); ax.set_xlim(0, 20)
|
||||
|
||||
# (f) SER @ 10 dB 막대
|
||||
ax = fig.add_subplot(gs[1, 2]); ax.set_facecolor('white')
|
||||
ms = ['OFDMA', 'NOMA-SIC', 'Joint+Attn', KP]
|
||||
s10 = [res_all[m]['ser'][idx10] for m in ms]
|
||||
lb10 = ['OFDMA', 'NOMA-SIC', 'Joint\n+Attn', 'MAML+Attn\n(제안)']
|
||||
c10 = ['#546E7A', '#E65100', '#C62828', '#1565C0']
|
||||
bars = ax.bar(range(4), s10, color=c10, width=0.55,
|
||||
edgecolor='white', linewidth=1.2)
|
||||
ax.set_xticks(range(4)); ax.set_xticklabels(lb10, fontsize=9.5)
|
||||
ax.set_ylabel('SER @ 10 dB', fontsize=11)
|
||||
ax.set_title('(f) 방법별 SER @ 10 dB', fontsize=12, fontweight='bold')
|
||||
ax.grid(True, alpha=0.3, axis='y')
|
||||
for b, v, c in zip(bars, s10, c10):
|
||||
ax.text(b.get_x() + b.get_width() / 2, v + 0.003, f'{v:.3f}',
|
||||
ha='center', va='bottom', fontsize=10, fontweight='bold', color=c)
|
||||
|
||||
# (g) 상관계수 — Joint
|
||||
ax = fig.add_subplot(gs[2, 0]); ax.set_facecolor('white')
|
||||
im = ax.imshow(rho_j, cmap=cmap_r, vmin=-0.3, vmax=0.3, aspect='auto')
|
||||
ax.set_xticks(range(U)); ax.set_yticks(range(U))
|
||||
ax.set_xticklabels([f'U{i+1}' for i in range(U)], fontsize=10)
|
||||
ax.set_yticklabels([f'U{i+1}' for i in range(U)], fontsize=10)
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
v = rho_j[i, j]
|
||||
ax.text(j, i, f'{v:.3f}', ha='center', va='center', fontsize=11,
|
||||
fontweight='bold', color='white' if abs(v) > 0.15 else 'black')
|
||||
plt.colorbar(im, ax=ax, fraction=0.046)
|
||||
ax.set_title('(g) 상관계수 — Joint Training', fontsize=12, fontweight='bold')
|
||||
ax.set_xlabel('사용자 j', fontsize=10); ax.set_ylabel('사용자 i', fontsize=10)
|
||||
|
||||
# (h) 상관계수 — MAML
|
||||
ax = fig.add_subplot(gs[2, 1]); ax.set_facecolor('white')
|
||||
im = ax.imshow(rho_m, cmap=cmap_r, vmin=-0.3, vmax=0.3, aspect='auto')
|
||||
ax.set_xticks(range(U)); ax.set_yticks(range(U))
|
||||
ax.set_xticklabels([f'U{i+1}' for i in range(U)], fontsize=10)
|
||||
ax.set_yticklabels([f'U{i+1}' for i in range(U)], fontsize=10)
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
v = rho_m[i, j]
|
||||
ax.text(j, i, f'{v:.3f}', ha='center', va='center', fontsize=11,
|
||||
fontweight='bold', color='white' if abs(v) > 0.15 else 'black')
|
||||
plt.colorbar(im, ax=ax, fraction=0.046)
|
||||
ax.set_title('(h) 상관계수 — MAML (제안)', fontsize=12, fontweight='bold')
|
||||
ax.set_xlabel('사용자 j', fontsize=10); ax.set_ylabel('사용자 i', fontsize=10)
|
||||
|
||||
# (i) Attention heatmap
|
||||
ax = fig.add_subplot(gs[2, 2]); ax.set_facecolor('white')
|
||||
im = ax.imshow(attn_m_mc, cmap=cmap_a,
|
||||
vmin=0, vmax=attn_m_mc.max(), aspect='auto')
|
||||
sh = ['보행자\n(U1)', '신호등\n(U2)', '차선\n(U3)', '속도\n(U4)']
|
||||
ax.set_xticks(range(U)); ax.set_yticks(range(U))
|
||||
ax.set_xticklabels(sh[:U], fontsize=9); ax.set_yticklabels(sh[:U], fontsize=9)
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
v = attn_m_mc[i, j]
|
||||
ax.text(j, i, f'{v:.3f}', ha='center', va='center', fontsize=11,
|
||||
fontweight='bold',
|
||||
color='white' if v > attn_m_mc.max() * 0.5 else '#0D1B3E')
|
||||
plt.colorbar(im, ax=ax, fraction=0.046)
|
||||
ax.set_title(f'(i) 어텐션 가중치 α_{{u,i}} — MAML @ 10dB',
|
||||
fontsize=12, fontweight='bold')
|
||||
ax.set_xlabel('참조 사용자 i', fontsize=10)
|
||||
ax.set_ylabel('질의 사용자 u', fontsize=10)
|
||||
|
||||
fig.suptitle(
|
||||
'Multi-User Semantic Communication: User-Wise Attention vs Orthogonal Allocation\n'
|
||||
f'(자율주행 시나리오 — U={U}, d={cfg.d}, Rayleigh Fading)',
|
||||
fontsize=13, fontweight='bold', y=0.97)
|
||||
|
||||
plt.savefig(out_path, dpi=150, bbox_inches='tight', facecolor='#F8F9FA')
|
||||
plt.close()
|
||||
print(f" 그림 저장 → {out_path}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 9. SUMMARY PRINT (semantic_sim.py 스타일)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def print_summary(res_maml, res_joint, res_ofdma, res_noma,
|
||||
rho_j, rho_m, attn_m, cfg):
|
||||
SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step)
|
||||
idx = {4: int((4 - cfg.snr_min) / cfg.snr_step),
|
||||
10: int((10 - cfg.snr_min) / cfg.snr_step),
|
||||
16: int((16 - cfg.snr_min) / cfg.snr_step)}
|
||||
U = cfg.U
|
||||
mask = ~np.eye(U, dtype=bool)
|
||||
|
||||
print("\n" + "=" * 62)
|
||||
print("NUMERICAL SUMMARY")
|
||||
print("=" * 62)
|
||||
print(f"{'Method':<22}{'SER@4dB':>9}{'SER@10dB':>10}"
|
||||
f"{'SER@16dB':>10}{'Cos@10dB':>10}")
|
||||
print("-" * 62)
|
||||
for lb, res in [('OFDMA', res_ofdma), ('NOMA-SIC', res_noma),
|
||||
('Joint+Attn', res_joint), ('MAML+Attn (제안)', res_maml)]:
|
||||
print(f"{lb:<22}{res['ser'][idx[4]]:>9.4f}"
|
||||
f"{res['ser'][idx[10]]:>10.4f}"
|
||||
f"{res['ser'][idx[16]]:>10.4f}"
|
||||
f"{res['cos'][idx[10]]:>10.4f}")
|
||||
|
||||
print(f"\n임베딩 상관계수 |ρ| (off-diag @ 10 dB):")
|
||||
print(f" Joint : mean={np.abs(rho_j[mask]).mean():.4f} "
|
||||
f"[{rho_j[mask].min():.4f}, {rho_j[mask].max():.4f}]")
|
||||
print(f" MAML : mean={np.abs(rho_m[mask]).mean():.4f} "
|
||||
f"[{rho_m[mask].min():.4f}, {rho_m[mask].max():.4f}]")
|
||||
|
||||
print(f"\n어텐션 가중치 α (MAML @ 10 dB):")
|
||||
hdr = ''.join([f" U{j+1}" for j in range(U)])
|
||||
print(f"{'':>14}{hdr}")
|
||||
names = ['보행자', '신호등', '차선 ', '속도 ']
|
||||
for i in range(U):
|
||||
row = ''.join([f" {attn_m[i,j]:>7.4f}" for j in range(U)])
|
||||
nm = names[i] if i < len(names) else f'U{i+1} '
|
||||
print(f" U{i+1}({nm}){row}")
|
||||
|
||||
print(f"\n핵심: α[보행자→신호등]={attn_m[0,1]:.4f} (높음) vs "
|
||||
f"α[보행자→속도]={attn_m[0,3]:.4f} (낮음)")
|
||||
print(f" SER 개선 vs OFDMA @ 10dB: "
|
||||
f"{res_ofdma['ser'][idx[10]] - res_maml['ser'][idx[10]]:.4f}")
|
||||
print("=" * 62)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 10. JSON EXPORT (for overlay in semantic_correlation_sim.py)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
import json
|
||||
|
||||
def export_results_json(scenario: str, SNR_DB, res_maml, res_joint,
|
||||
res_ofdma, res_noma, outdir: Path):
|
||||
"""Export SER results to JSON so semantic_correlation_sim.py can overlay them."""
|
||||
data = {
|
||||
"scenario": scenario,
|
||||
"decoder_only": True,
|
||||
"snr_db": SNR_DB.tolist(),
|
||||
"maml_ser": res_maml["ser"].tolist(),
|
||||
"joint_ser": res_joint["ser"].tolist(),
|
||||
"ofdma_ser": res_ofdma["ser"].tolist(),
|
||||
"noma_ser": res_noma["ser"].tolist(),
|
||||
}
|
||||
path = outdir / f"trained_{scenario}.json"
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(f"결과 JSON 저장 → {path}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 11. MAIN
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def _run_one_scenario(scenario: str, cfg, device, np_rng, outdir: Path):
|
||||
"""Train + evaluate one scenario; save checkpoint + JSON."""
|
||||
scenario_cfg = SCENARIO_CONFIGS.get(scenario) # None for DEFAULT falls back to BLEND
|
||||
|
||||
def make_model():
|
||||
return SemanticCommSystem(cfg.d, cfg.U, cfg.H,
|
||||
decoder_only=cfg.decoder_only).to(device)
|
||||
|
||||
maml_model = make_model()
|
||||
joint_model = make_model()
|
||||
|
||||
SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step)
|
||||
|
||||
# ── 학습 ──────────────────────────────────────────────────────────
|
||||
tag = f"[{scenario}]"
|
||||
print(f"\n{'='*60}\n{tag} decoder_only={cfg.decoder_only}\n{'='*60}")
|
||||
|
||||
trainer = MAMLTrainer(maml_model, cfg, device, np_rng, scenario_cfg)
|
||||
hist_m = trainer.train()
|
||||
hist_j = train_joint(joint_model, cfg, device, np_rng, scenario_cfg)
|
||||
|
||||
ckpt_path = outdir / f"models_{scenario}.pt"
|
||||
torch.save({"maml": maml_model.state_dict(),
|
||||
"joint": joint_model.state_dict()}, ckpt_path)
|
||||
print(f"체크포인트 저장 → {ckpt_path}")
|
||||
|
||||
# 학습 곡선
|
||||
fig, ax = plt.subplots(figsize=(7, 4))
|
||||
ax.plot(hist_m, label='MAML outer loss', color='#1565C0', lw=1.5)
|
||||
ax.plot(hist_j, label='Joint loss', color='#C62828', lw=1.5, ls='--')
|
||||
ax.set_xlabel('Epoch'); ax.set_ylabel('Loss')
|
||||
ax.set_title(f'Training Loss — {scenario}')
|
||||
ax.legend(); ax.grid(True, alpha=0.35); fig.tight_layout()
|
||||
fig.savefig(str(outdir / f"training_curves_{scenario}.png"), dpi=150)
|
||||
plt.close()
|
||||
|
||||
# ── 평가 ──────────────────────────────────────────────────────────
|
||||
print(f"{tag} 평가 중 ...")
|
||||
res_maml = evaluate_model(maml_model, cfg, device, np_rng, "rayleigh", scenario_cfg)
|
||||
res_joint = evaluate_model(joint_model, cfg, device, np_rng, "rayleigh", scenario_cfg)
|
||||
res_ofdma = evaluate_model(joint_model, cfg, device, np_rng, "ofdma", scenario_cfg)
|
||||
res_noma = evaluate_model(joint_model, cfg, device, np_rng, "noma", scenario_cfg)
|
||||
|
||||
export_results_json(scenario, SNR_DB, res_maml, res_joint,
|
||||
res_ofdma, res_noma, outdir)
|
||||
|
||||
# SNR=10dB 상관계수 & attention map
|
||||
idx10 = int((10 - cfg.snr_min) / cfg.snr_step)
|
||||
rho_j = res_joint["rho"][idx10]
|
||||
rho_m = res_maml["rho"][idx10]
|
||||
attn_m = get_attention_map(maml_model, 10.0, cfg, device, np_rng)
|
||||
|
||||
print_summary(res_maml, res_joint, res_ofdma, res_noma,
|
||||
rho_j, rho_m, attn_m, cfg)
|
||||
|
||||
# 단일 시나리오 그림 저장 (DEFAULT는 기존 filename 유지)
|
||||
suffix = "" if scenario == "DEFAULT" else f"_{scenario}"
|
||||
fig_path = str(outdir / f"semantic_results{suffix}.png")
|
||||
plot_results(SNR_DB, res_maml, res_joint, res_ofdma, res_noma,
|
||||
rho_j, rho_m, attn_m, fig_path, cfg)
|
||||
return res_maml, res_joint, res_ofdma, res_noma
|
||||
|
||||
|
||||
def main():
|
||||
cfg = get_cfg()
|
||||
torch.manual_seed(cfg.seed)
|
||||
np_rng = np.random.default_rng(cfg.seed)
|
||||
|
||||
if cfg.device == "auto":
|
||||
device = torch.device(
|
||||
"cuda" if torch.cuda.is_available() else
|
||||
"mps" if torch.backends.mps.is_available() else
|
||||
"cpu")
|
||||
else:
|
||||
device = torch.device(cfg.device)
|
||||
print(f"Device: {device} | d={cfg.d}, U={cfg.U}, "
|
||||
f"meta_epochs={cfg.meta_epochs}, n_mc={cfg.n_mc}, "
|
||||
f"scenario={cfg.scenario}, decoder_only={cfg.decoder_only}")
|
||||
|
||||
outdir = Path(cfg.outdir)
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# eval_only mode (legacy)
|
||||
if cfg.eval_only and cfg.ckpt:
|
||||
SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step)
|
||||
scenario_cfg = SCENARIO_CONFIGS.get(cfg.scenario)
|
||||
maml_model = SemanticCommSystem(cfg.d, cfg.U, cfg.H,
|
||||
decoder_only=cfg.decoder_only).to(device)
|
||||
joint_model = SemanticCommSystem(cfg.d, cfg.U, cfg.H,
|
||||
decoder_only=cfg.decoder_only).to(device)
|
||||
ck = torch.load(cfg.ckpt, map_location=device)
|
||||
maml_model.load_state_dict(ck["maml"])
|
||||
joint_model.load_state_dict(ck["joint"])
|
||||
print(f"체크포인트 로드 ← {cfg.ckpt}")
|
||||
res_maml = evaluate_model(maml_model, cfg, device, np_rng, "rayleigh", scenario_cfg)
|
||||
res_joint = evaluate_model(joint_model, cfg, device, np_rng, "rayleigh", scenario_cfg)
|
||||
res_ofdma = evaluate_model(joint_model, cfg, device, np_rng, "ofdma", scenario_cfg)
|
||||
res_noma = evaluate_model(joint_model, cfg, device, np_rng, "noma", scenario_cfg)
|
||||
export_results_json(cfg.scenario, SNR_DB, res_maml, res_joint,
|
||||
res_ofdma, res_noma, outdir)
|
||||
print(f"\n완료! → {outdir}/")
|
||||
return
|
||||
|
||||
# Train / eval scenarios
|
||||
if cfg.scenario.upper() == "ALL":
|
||||
scenarios = ['HIGH', 'LOW', 'MIX', 'HETERO', 'ASYM']
|
||||
else:
|
||||
scenarios = [cfg.scenario]
|
||||
|
||||
for sc in scenarios:
|
||||
_run_one_scenario(sc, cfg, device, np_rng, outdir)
|
||||
|
||||
print(f"\n완료! → {outdir}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+1044
File diff suppressed because it is too large
Load Diff
Executable
+124
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Ablation / sensitivity studies for the TWC revision.
|
||||
Part 1 (torch): H (heads), K (number of SNR meta-tasks), S (inner steps)
|
||||
sensitivity -> SER@10dB, cos@10dB, |rho_off|@10dB on HIGH.
|
||||
Part 2 (numpy): refined scalability -> mean cosine + top-k retention vs U.
|
||||
"""
|
||||
import types
|
||||
import numpy as np
|
||||
import torch
|
||||
import maml_semantic as M
|
||||
|
||||
torch.manual_seed(0)
|
||||
|
||||
class _Sim: # minimal stand-ins (avoid importing the heavy sim module)
|
||||
BATCH = 64
|
||||
@staticmethod
|
||||
def _norm(E):
|
||||
return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8)
|
||||
@staticmethod
|
||||
def cos_sim(Eh, Eg):
|
||||
return (Eh * Eg).sum(-1)
|
||||
sim = _Sim()
|
||||
|
||||
def make_cfg(**kw):
|
||||
base = dict(d=64, U=4, H=4, tau=0.45, lam=0.1, snr_min=0.0, snr_max=20.0,
|
||||
snr_step=2.0, inner_lr=0.01, inner_steps=5, outer_lr=1e-3,
|
||||
meta_epochs=70, joint_epochs=70, batch=64, n_mc=60, seed=42,
|
||||
scenario='HIGH', decoder_only=True)
|
||||
base.update(kw)
|
||||
return types.SimpleNamespace(**base)
|
||||
|
||||
def train_eval(cfg):
|
||||
device = 'cpu'
|
||||
rng = np.random.default_rng(cfg.seed)
|
||||
scen = M.SCENARIO_CONFIGS[cfg.scenario]
|
||||
model = M.SemanticCommSystem(cfg.d, cfg.U, cfg.H, decoder_only=cfg.decoder_only).to(device)
|
||||
tr = M.MAMLTrainer(model, cfg, device, rng, scen)
|
||||
tr.train()
|
||||
res = M.evaluate_model(model, cfg, device, rng, "rayleigh", scen)
|
||||
snr = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step)
|
||||
i10 = int(np.argmin(np.abs(snr - 10)))
|
||||
rho = res['rho'][i10]
|
||||
mask = ~np.eye(cfg.U, dtype=bool)
|
||||
return res['ser'][i10], res['cos'][i10], float(np.abs(rho[mask]).mean()), len(snr)
|
||||
|
||||
def part1():
|
||||
print("\n=== ABLATION (HIGH scenario, decoder-only, SNR=10 dB) ===")
|
||||
print("\n-- Attention heads H (d=64) --")
|
||||
for H in [1, 2, 4, 8]:
|
||||
ser, cos, rho, _ = train_eval(make_cfg(H=H))
|
||||
print(f" H={H}: dk={64//H:2d} SER={ser:.3f} cos={cos:.3f} |rho_off|={rho:.3f}")
|
||||
print("\n-- Number of SNR meta-tasks K (via snr_step) --")
|
||||
for step in [20.0, 10.0, 4.0, 2.0, 1.0]:
|
||||
ser, cos, rho, K = train_eval(make_cfg(snr_step=step))
|
||||
print(f" K={K:2d} (step={step:>4}): SER={ser:.3f} cos={cos:.3f} |rho_off|={rho:.3f}")
|
||||
print("\n-- Inner-loop steps S --")
|
||||
for S in [1, 3, 5, 10]:
|
||||
ser, cos, rho, _ = train_eval(make_cfg(inner_steps=S))
|
||||
print(f" S={S:2d}: SER={ser:.3f} cos={cos:.3f} |rho_off|={rho:.3f}")
|
||||
|
||||
# ---- Part 2: refined scalability (cosine + top-k retention) ----
|
||||
RNG = np.random.default_rng(11)
|
||||
|
||||
def gen_clustered(n, U, D, g, beta):
|
||||
nc = U // g
|
||||
scenes = [RNG.standard_normal(D) for _ in range(nc)]
|
||||
scenes = [s / np.linalg.norm(s) for s in scenes]
|
||||
embs = []
|
||||
for u in range(U):
|
||||
s = scenes[u // g]
|
||||
priv = RNG.standard_normal((n, D)); priv /= np.linalg.norm(priv, axis=-1, keepdims=True) + 1e-8
|
||||
e = np.sqrt(1 - beta**2) * priv + beta * s[None, :]
|
||||
e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8
|
||||
embs.append(e)
|
||||
return np.stack(embs, 1)
|
||||
|
||||
def masks_for(U, D):
|
||||
dpu = D // U; Mk = np.zeros((U, D))
|
||||
for u in range(U):
|
||||
Mk[u, u*dpu:(u+1)*dpu] = 1.0
|
||||
return Mk
|
||||
|
||||
def se_chan(E, snr_db, Mk):
|
||||
n, U, D = E.shape
|
||||
Ytx = (E * Mk[None]).sum(1)
|
||||
h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) * np.sqrt(0.5)
|
||||
nstd = np.sqrt(float(np.mean(Ytx**2)) / (10**(snr_db/10)))
|
||||
return h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * nstd
|
||||
|
||||
def attn(Yrx, Mk, bm, topk=None):
|
||||
n, U, D = Yrx.shape
|
||||
R = Yrx[:, :, None, :] * Mk[None, None]
|
||||
a = bm.copy(); np.fill_diagonal(a, 1.0)
|
||||
if topk is not None and topk < U:
|
||||
for u in range(U):
|
||||
order = np.argsort(-a[u]); keep = set(order[:topk]) | {u}
|
||||
for v in range(U):
|
||||
if v not in keep: a[u, v] = 0.0
|
||||
a /= a.sum(1, keepdims=True) + 1e-8
|
||||
ctx = np.einsum('ui,buid->bud', a, R)
|
||||
return np.stack([sim._norm(ctx[:, u, :]) for u in range(U)], 1)
|
||||
|
||||
def part2():
|
||||
print("\n=== SCALABILITY: cosine + top-k retention (g=4, beta=0.65, SNR=20 dB) ===")
|
||||
g, beta, snr = 4, 0.65, 20.0
|
||||
for U in [4, 8, 16, 32]:
|
||||
D = 16 * U; Mk = masks_for(U, D)
|
||||
bm = np.zeros((U, U))
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
if i // g == j // g: bm[i, j] = beta*beta
|
||||
nmc = 150; cf = ct = 0.0
|
||||
for _ in range(nmc):
|
||||
E = gen_clustered(sim.BATCH, U, D, g, beta)
|
||||
cf += sim.cos_sim(attn(se_chan(E, snr, Mk), Mk, bm), E).mean()
|
||||
ct += sim.cos_sim(attn(se_chan(E, snr, Mk), Mk, bm, topk=g), E).mean()
|
||||
cf /= nmc; ct /= nmc
|
||||
print(f" U={U:3d} cos_full={cf:.3f} cos_topk(k={g})={ct:.3f} "
|
||||
f"retention={100*ct/cf:5.1f}% ops full={U*U} topk={U*g} ({U//g}x)")
|
||||
|
||||
if __name__ == '__main__':
|
||||
part1()
|
||||
part2()
|
||||
print("\nDONE.")
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
"""Fig. 3 (SER gain vs relevance coefficient) regenerated with the SAME box
|
||||
aspect (0.8) as the Fig. 2 panels, from results/data/beta_sweep.csv."""
|
||||
import csv, numpy as np
|
||||
import matplotlib; matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
rows = list(csv.DictReader(open('results/data/beta_sweep.csv')))
|
||||
SNRS = [0.0, 5.0, 10.0]
|
||||
STY = {0.0: ('#C62828', 's--', 'SNR = 0 dB'),
|
||||
5.0: ('#E65100', '^-.', 'SNR = 5 dB'),
|
||||
10.0: ('#1565C0', 'o-', 'SNR = 10 dB')}
|
||||
FILL = {0.0: '#C62828', 5.0: '#E65100', 10.0: '#1565C0'}
|
||||
|
||||
data = {s: {'b': [], 'g': []} for s in SNRS}
|
||||
for r in rows:
|
||||
s = float(r['snr_label'])
|
||||
if s in data:
|
||||
data[s]['b'].append(float(r['beta_sq'])); data[s]['g'].append(float(r['gain_maml']))
|
||||
for s in SNRS:
|
||||
o = np.argsort(data[s]['b'])
|
||||
data[s]['b'] = np.array(data[s]['b'])[o]; data[s]['g'] = np.array(data[s]['g'])[o]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(5.2, 4.6))
|
||||
for s in SNRS:
|
||||
c, mk, lbl = STY[s]
|
||||
ax.plot(data[s]['b'], data[s]['g'], mk, lw=2.0, color=c, label=lbl, markersize=5)
|
||||
ax.fill_between(data[s]['b'], 0, data[s]['g'], alpha=0.07, color=FILL[s])
|
||||
ax.axhline(0, color='gray', lw=0.8, ls=':')
|
||||
ax.set_xlabel(r'Semantic relevance coefficient $\beta_{u,v}=\beta_u\cdot\beta_v$', fontsize=13)
|
||||
ax.set_ylabel('SER gain over OFDMA', fontsize=13)
|
||||
ax.tick_params(labelsize=12)
|
||||
ax.legend(loc='upper left', fontsize=12)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.set_xlim(-0.01, 0.82)
|
||||
ax.set_box_aspect(0.8) # match the Fig. 2 panel box aspect
|
||||
fig.tight_layout()
|
||||
fig.savefig('results/fig4_beta_sweep.pdf', bbox_inches='tight')
|
||||
fig.savefig('results/fig4_beta_sweep.png', dpi=150, bbox_inches='tight')
|
||||
print('saved results/fig4_beta_sweep.pdf (box_aspect=0.8)')
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import types, numpy as np, torch
|
||||
import maml_semantic as M
|
||||
torch.manual_seed(0)
|
||||
def cfg(**k):
|
||||
b=dict(d=64,U=4,H=4,tau=0.45,lam=0.1,snr_min=0.0,snr_max=20.0,snr_step=2.0,
|
||||
inner_lr=0.01,inner_steps=5,outer_lr=1e-3,meta_epochs=70,joint_epochs=70,
|
||||
batch=64,n_mc=60,seed=42,scenario='HIGH',decoder_only=True); b.update(k)
|
||||
return types.SimpleNamespace(**b)
|
||||
def run(d):
|
||||
c=cfg(d=d); dev='cpu'; rng=np.random.default_rng(42); scen=M.SCENARIO_CONFIGS['HIGH']
|
||||
m=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=True).to(dev)
|
||||
M.MAMLTrainer(m,c,dev,rng,scen).train()
|
||||
res=M.evaluate_model(m,c,dev,rng,'rayleigh',scen)
|
||||
snr=np.arange(0,20.0001,2); i=int(np.argmin(np.abs(snr-10)))
|
||||
rho=res['rho'][i]; mask=~np.eye(c.U,dtype=bool)
|
||||
return res['ser'][i],res['cos'][i],float(np.abs(rho[mask]).mean())
|
||||
print("=== d sweep (HIGH, SNR=10dB) ===")
|
||||
for d in [32,64,128]:
|
||||
s,co,r=run(d); print(f"d={d:3d}: SER={s:.3f} cos={co:.3f} |rho_off|={r:.3f}")
|
||||
print("DONE")
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
"""End-to-end (learnable encoder) vs decoder-only MAML, HIGH scenario.
|
||||
Verifies Reviewer-1 Comment-3 claim that cross-attention stays stable and
|
||||
effective when the encoder is also learned."""
|
||||
import types, numpy as np, torch
|
||||
import maml_semantic as M
|
||||
torch.manual_seed(0)
|
||||
|
||||
def cfg(**k):
|
||||
b = dict(d=64, U=4, H=4, tau=0.45, lam=0.1, snr_min=0.0, snr_max=20.0,
|
||||
snr_step=2.0, inner_lr=0.01, inner_steps=5, outer_lr=1e-3,
|
||||
meta_epochs=120, joint_epochs=120, batch=64, n_mc=80, seed=42,
|
||||
scenario='HIGH', decoder_only=True)
|
||||
b.update(k); return types.SimpleNamespace(**b)
|
||||
|
||||
def run(decoder_only):
|
||||
c = cfg(decoder_only=decoder_only); dev='cpu'; rng=np.random.default_rng(c.seed)
|
||||
scen=M.SCENARIO_CONFIGS[c.scenario]
|
||||
mdl=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=decoder_only).to(dev)
|
||||
M.MAMLTrainer(mdl,c,dev,rng,scen).train()
|
||||
res=M.evaluate_model(mdl,c,dev,rng,"rayleigh",scen)
|
||||
snr=np.arange(0,20.0001,2);
|
||||
def at(s): return res['ser'][int(np.argmin(np.abs(snr-s)))], res['cos'][int(np.argmin(np.abs(snr-s)))]
|
||||
i10=int(np.argmin(np.abs(snr-10))); rho=res['rho'][i10]; mask=~np.eye(c.U,dtype=bool)
|
||||
return at(4), at(10), at(16), float(np.abs(rho[mask]).mean())
|
||||
|
||||
print("=== END-TO-END (learnable encoder) vs DECODER-ONLY, HIGH ===")
|
||||
for tag, do in [("decoder-only (frozen enc)", True), ("end-to-end (learnable enc)", False)]:
|
||||
(s4,c4),(s10,c10),(s16,c16),rho = run(do)
|
||||
print(f" {tag:30s} SER@4/10/16 = {s4:.3f}/{s10:.3f}/{s16:.3f} "
|
||||
f"cos@10={c10:.3f} |rho_off|@10={rho:.3f}")
|
||||
print("DONE.")
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Auxiliary experiments for the TWC revision (main_FFF.tex).
|
||||
Reuses the validated simulation primitives in semantic_correlation_sim.py to
|
||||
produce REAL numbers for the new reviewer-requested studies:
|
||||
|
||||
A. Threshold sensitivity (tau = 0.30..0.50) + mean cosine similarity [R1.2, R3.2]
|
||||
B. Residual phase-error robustness [R1.1, R2.3]
|
||||
C. User scaling U in {4,8,16,32} : full vs. sparse top-k attention [R1.6, R3.4]
|
||||
D. DL semantic baseline: Joint+Attn calibrated at a single nominal SNR [R1.5,R2.5,R3.5]
|
||||
vs. MAML SNR-adaptive shrinkage
|
||||
|
||||
All results are printed as LaTeX-ready rows.
|
||||
"""
|
||||
import numpy as np
|
||||
import semantic_correlation_sim as sim
|
||||
|
||||
RNG = np.random.default_rng(2026)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# A. Threshold sensitivity + mean cosine similarity (HIGH/LOW/MIX, SNR=10 dB)
|
||||
# ----------------------------------------------------------------------------
|
||||
def collect_cos(scenario_key, snr=10.0, n_mc=600):
|
||||
cfg = sim.SCENARIOS[scenario_key]
|
||||
beta_mat = sim.compute_beta_matrix(cfg)
|
||||
out = {'OFDMA': [], 'NOMA-SIC': [], 'UWCA': []}
|
||||
for _ in range(n_mc):
|
||||
Egt = sim.gen_embeddings(sim.BATCH, scenario_key)
|
||||
Y = sim.shared_embedding_channel(Egt, snr)
|
||||
Eh, _ = sim.ofdma_se_decoder(Y)
|
||||
out['OFDMA'].append(sim.cos_sim(Eh, Egt).ravel())
|
||||
y, h = sim.noma_ul_channel(Egt, snr)
|
||||
Eh = sim.noma_sic_decoder(y, h)
|
||||
out['NOMA-SIC'].append(sim.cos_sim(Eh, Egt).ravel())
|
||||
Y = sim.shared_embedding_channel(Egt, snr)
|
||||
Eh, _ = sim.maml_attention_se_decoder(Y, snr, beta_mat)
|
||||
out['UWCA'].append(sim.cos_sim(Eh, Egt).ravel())
|
||||
return {k: np.concatenate(v) for k, v in out.items()}
|
||||
|
||||
def exp_A():
|
||||
print("\n=== EXP A: threshold sensitivity + mean cosine (SNR=10 dB) ===")
|
||||
taus = [0.30, 0.35, 0.40, 0.45, 0.50]
|
||||
for scen in ['HIGH', 'LOW', 'MIX']:
|
||||
cos = collect_cos(scen)
|
||||
print(f"\n[{scen}]")
|
||||
for m in ['OFDMA', 'NOMA-SIC', 'UWCA']:
|
||||
c = cos[m]
|
||||
sers = [f"{(c < t).mean():.3f}" for t in taus]
|
||||
print(f" {m:9s} meancos={c.mean():.3f} SER@tau[{','.join(map(str,taus))}] = {sers}")
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# B. Residual phase-error robustness
|
||||
# After imperfect pilot-based compensation, residual phase Dphi ~ N(0,sig^2);
|
||||
# recovered in-phase component scales by cos(Dphi) (quadrature energy lost).
|
||||
# ----------------------------------------------------------------------------
|
||||
def se_channel_phase(E, snr_db, sigma_phi_deg):
|
||||
n, U, D = E.shape
|
||||
X = E * sim.MASKS[None, :, :]
|
||||
Ytx = X.sum(axis=1)
|
||||
h = (np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2)
|
||||
* np.sqrt(0.5))
|
||||
sig_power = float(np.mean(Ytx**2))
|
||||
noise_std = np.sqrt(sig_power / (10**(snr_db/10)))
|
||||
phi = np.deg2rad(sigma_phi_deg) * RNG.standard_normal((n, U, 1))
|
||||
Yrx = h * np.cos(phi) * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * noise_std
|
||||
return Yrx
|
||||
|
||||
def exp_B():
|
||||
print("\n=== EXP B: residual phase-error robustness (HIGH scenario) ===")
|
||||
cfg = sim.SCENARIOS['HIGH']; beta_mat = sim.compute_beta_matrix(cfg)
|
||||
for snr in [10.0, 20.0]:
|
||||
row = []
|
||||
for sig in [0, 5, 10, 15, 20]:
|
||||
acc = 0.0; n_mc = 400
|
||||
for _ in range(n_mc):
|
||||
Egt = sim.gen_embeddings(sim.BATCH, 'HIGH')
|
||||
Y = se_channel_phase(Egt, snr, sig)
|
||||
Eh, _ = sim.maml_attention_se_decoder(Y, snr, beta_mat)
|
||||
acc += sim.ser_total(Eh, Egt)
|
||||
row.append(f"{acc/n_mc:.3f}")
|
||||
print(f" SNR={snr:4.0f}dB UWCA-SER vs sigma_phi[0,5,10,15,20 deg] = {row}")
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# C. User scaling + sparse top-k attention (clustered relevance)
|
||||
# U users in clusters of size g sharing a scene; cross-cluster beta=0.
|
||||
# full attention: O(U^2) ; top-k (k=g): O(U*k).
|
||||
# ----------------------------------------------------------------------------
|
||||
def gen_clustered(n, U, D, g, beta):
|
||||
n_clusters = U // g
|
||||
scenes = []
|
||||
for _ in range(n_clusters):
|
||||
s = RNG.standard_normal(D); scenes.append(s / np.linalg.norm(s))
|
||||
embs = []
|
||||
for u in range(U):
|
||||
s = scenes[u // g]
|
||||
priv = RNG.standard_normal((n, D))
|
||||
priv /= np.linalg.norm(priv, axis=-1, keepdims=True) + 1e-8
|
||||
e = np.sqrt(1 - beta**2) * priv + beta * s[None, :]
|
||||
e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8
|
||||
embs.append(e)
|
||||
return np.stack(embs, axis=1)
|
||||
|
||||
def masks_for(U, D):
|
||||
dpu = D // U
|
||||
M = np.zeros((U, D))
|
||||
for u in range(U):
|
||||
M[u, u*dpu:(u+1)*dpu] = 1.0
|
||||
return M
|
||||
|
||||
def se_channel_generic(E, snr_db, M):
|
||||
n, U, D = E.shape
|
||||
X = E * M[None, :, :]
|
||||
Ytx = X.sum(axis=1)
|
||||
h = (np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2)
|
||||
* np.sqrt(0.5))
|
||||
noise_std = np.sqrt(float(np.mean(Ytx**2)) / (10**(snr_db/10)))
|
||||
return h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * noise_std
|
||||
|
||||
def attn_decode(Yrx, M, beta_mat, topk=None):
|
||||
n, U, D = Yrx.shape
|
||||
R = Yrx[:, :, None, :] * M[None, None, :, :] # (n,U,U,D)
|
||||
alpha = beta_mat.copy(); np.fill_diagonal(alpha, 1.0)
|
||||
if topk is not None and topk < U:
|
||||
# keep self + top-(k-1) strongest cross weights per row
|
||||
for u in range(U):
|
||||
order = np.argsort(-alpha[u])
|
||||
keep = set(order[:topk].tolist()) | {u}
|
||||
for v in range(U):
|
||||
if v not in keep:
|
||||
alpha[u, v] = 0.0
|
||||
alpha /= alpha.sum(1, keepdims=True) + 1e-8
|
||||
ctx = np.einsum('ui,buid->bud', alpha, R)
|
||||
Eh = np.stack([sim._norm(ctx[:, u, :]) for u in range(U)], axis=1)
|
||||
return Eh
|
||||
|
||||
def exp_C():
|
||||
print("\n=== EXP C: user scaling + sparse top-k attention (g=4, beta=0.65, SNR=10 dB) ===")
|
||||
g = 4; beta = 0.65; snr = 10.0
|
||||
for U in [4, 8, 16, 32]:
|
||||
D = 16 * U # keep 16 dims/user
|
||||
M = masks_for(U, D)
|
||||
bm = np.zeros((U, U))
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
if i // g == j // g:
|
||||
bm[i, j] = beta * beta
|
||||
n_mc = 200
|
||||
ser_full = ser_topk = 0.0
|
||||
for _ in range(n_mc):
|
||||
Egt = gen_clustered(sim.BATCH, U, D, g, beta)
|
||||
Y = se_channel_generic(Egt, snr, M)
|
||||
ser_full += float((sim.cos_sim(attn_decode(Y, M, bm), Egt) < 0.45).mean())
|
||||
Y2 = se_channel_generic(Egt, snr, M)
|
||||
ser_topk += float((sim.cos_sim(attn_decode(Y2, M, bm, topk=g), Egt) < 0.45).mean())
|
||||
ops_full = U * U
|
||||
ops_topk = U * g
|
||||
print(f" U={U:3d} SER_full={ser_full/n_mc:.3f} SER_topk(k={g})={ser_topk/n_mc:.3f}"
|
||||
f" attn_ops: full={ops_full} topk={ops_topk} reduction={ops_full/ops_topk:.1f}x")
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# D. DL semantic baseline: Joint+Attn calibrated at single nominal SNR (10 dB)
|
||||
# vs. MAML SNR-adaptive shrinkage. Wiener-type shrinkage s = g/(g+1) applied
|
||||
# to the aggregated cross-attention context; MAML adapts s to the test SNR,
|
||||
# the non-meta Joint baseline is frozen at the training SNR.
|
||||
# ----------------------------------------------------------------------------
|
||||
def attn_decode_shrink(Yrx, M, beta_mat, shrink):
|
||||
n, U, D = Yrx.shape
|
||||
R = Yrx[:, :, None, :] * M[None, None, :, :]
|
||||
alpha = beta_mat.copy(); np.fill_diagonal(alpha, 0.0)
|
||||
alpha /= (alpha.sum(1, keepdims=True) + 1e-8)
|
||||
cross = np.einsum('ui,buid->bud', alpha, R) # cross context
|
||||
own = np.einsum('buud->bud', R.transpose(0,1,2,3)) # placeholder
|
||||
own = Yrx * M[None, :, :] # own subspace skip
|
||||
ctx = shrink * cross + own
|
||||
Eh = np.stack([sim._norm(ctx[:, u, :]) for u in range(U)], axis=1)
|
||||
return Eh
|
||||
|
||||
def exp_D():
|
||||
print("\n=== EXP D: DL baseline (Joint+Attn fixed 10 dB) vs MAML adaptive ===")
|
||||
cfg = sim.SCENARIOS['HIGH']; bm = sim.compute_beta_matrix(cfg)
|
||||
g0 = 10**(10/10); shrink_fixed = g0/(g0+1) # calibrated at 10 dB
|
||||
for snr in [0, 5, 10, 15, 20]:
|
||||
g = 10**(snr/10); shrink_adapt = g/(g+1)
|
||||
n_mc = 300; ser_fixed = ser_adapt = 0.0
|
||||
for _ in range(n_mc):
|
||||
Egt = sim.gen_embeddings(sim.BATCH, 'HIGH')
|
||||
Y = sim.shared_embedding_channel(Egt, snr)
|
||||
ser_fixed += float((sim.cos_sim(attn_decode_shrink(Y, sim.MASKS, bm, shrink_fixed), Egt) < 0.45).mean())
|
||||
Y2 = sim.shared_embedding_channel(Egt, snr)
|
||||
ser_adapt += float((sim.cos_sim(attn_decode_shrink(Y2, sim.MASKS, bm, shrink_adapt), Egt) < 0.45).mean())
|
||||
print(f" SNR={snr:3d}dB Joint+Attn(fixed10dB)={ser_fixed/n_mc:.3f} MAML-UWCA(adaptive)={ser_adapt/n_mc:.3f}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
exp_A()
|
||||
exp_B()
|
||||
exp_C()
|
||||
exp_D()
|
||||
print("\nDONE.")
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Real-data validation v2 (self-contained; no heavy import).
|
||||
sklearn digits (8x8=64-dim REAL images) -> mean-centered, unit-normalized
|
||||
embeddings. Mean removal decorrelates the shared ink/DC structure so the LOW
|
||||
scenario attains genuinely low inter-user relevance.
|
||||
|
||||
Adds the downstream task-accuracy metric (nearest class-prototype) alongside
|
||||
SER and mean cosine, and reports the empirical beta_uv per scenario.
|
||||
"""
|
||||
import numpy as np
|
||||
from sklearn.datasets import load_digits
|
||||
|
||||
RNG = np.random.default_rng(7)
|
||||
U, D = 4, 64
|
||||
DPU = D // U
|
||||
MASKS = np.zeros((U, D))
|
||||
for u in range(U):
|
||||
MASKS[u, u*DPU:(u+1)*DPU] = 1.0
|
||||
NOMA_POWER = np.array([0.40, 0.30, 0.20, 0.10])
|
||||
TAU = 0.45
|
||||
|
||||
X, y = load_digits(return_X_y=True)
|
||||
X = X.astype(np.float64)
|
||||
X = X - X.mean(0, keepdims=True) # remove shared DC structure
|
||||
X = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-8)
|
||||
by_class = {c: X[y == c] for c in range(10)}
|
||||
# class prototypes (gallery) for the downstream nearest-prototype classifier
|
||||
PROTO = np.stack([by_class[c].mean(0) for c in range(10)])
|
||||
PROTO = PROTO / (np.linalg.norm(PROTO, axis=1, keepdims=True) + 1e-8)
|
||||
|
||||
def _norm(E): return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8)
|
||||
def cos_sim(Eh, Eg): return (Eh * Eg).sum(-1)
|
||||
|
||||
def se_channel(E, snr_db):
|
||||
n = E.shape[0]
|
||||
X_ = E * MASKS[None]; Ytx = X_.sum(1)
|
||||
h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) * np.sqrt(0.5)
|
||||
nstd = np.sqrt(float(np.mean(Ytx**2)) / (10**(snr_db/10)))
|
||||
return h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * nstd
|
||||
|
||||
def ofdma_decode(Yrx):
|
||||
return np.stack([_norm(Yrx[:, u, :] * MASKS[u]) for u in range(U)], 1)
|
||||
|
||||
def noma_channel(E, snr_db):
|
||||
n = E.shape[0]
|
||||
h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) * np.sqrt(0.5)
|
||||
w = E * np.sqrt(NOMA_POWER)[None, :, None] * h
|
||||
yv = w.sum(1)
|
||||
nstd = np.sqrt(float(np.mean(yv**2)) / (10**(snr_db/10)))
|
||||
return yv + RNG.standard_normal((n, D)) * nstd, h
|
||||
|
||||
def noma_sic(yv, h):
|
||||
n = yv.shape[0]; Eh = np.zeros((n, U, D)); res = yv.copy()
|
||||
for u in range(U):
|
||||
z = res / (h[:, u, :] + 1e-8); Eh[:, u, :] = _norm(z)
|
||||
res -= h[:, u, :] * np.sqrt(NOMA_POWER[u]) * Eh[:, u, :]
|
||||
return Eh
|
||||
|
||||
def uwca_decode(Yrx, beta_mat):
|
||||
R = Yrx[:, :, None, :] * MASKS[None, None]
|
||||
a = beta_mat.copy(); np.fill_diagonal(a, 1.0); a /= a.sum(1, keepdims=True) + 1e-8
|
||||
ctx = np.einsum('ui,buid->bud', a, R)
|
||||
return np.stack([_norm(ctx[:, u, :]) for u in range(U)], 1)
|
||||
|
||||
SCEN = {'HIGH': [3, 3, 3, 3], 'LOW': [0, 1, 7, 4], 'MIX': [3, 3, 8, 1]}
|
||||
|
||||
def sample_users(n, ca):
|
||||
return np.stack([by_class[ca[u]][RNG.integers(0, len(by_class[ca[u]]), n)] for u in range(U)], 1)
|
||||
|
||||
def empirical_beta(ca, n=4000):
|
||||
E = sample_users(n, ca)
|
||||
B = np.einsum('nud,nvd->uv', E, E) / n
|
||||
return B
|
||||
|
||||
def downstream_acc(Eh, labels):
|
||||
# labels: (U,) true class per user; Eh: (n,U,D)
|
||||
sims = np.einsum('nud,cd->nuc', _norm(Eh), PROTO) # (n,U,10)
|
||||
pred = sims.argmax(-1) # (n,U)
|
||||
return (pred == np.array(labels)[None, :]).mean()
|
||||
|
||||
def run(scen, snr, n_mc=400):
|
||||
ca = SCEN[scen]; bm = empirical_beta(ca); bmc = bm.copy(); np.fill_diagonal(bmc, 0.0); bmc = np.clip(bmc, 0, None)
|
||||
M = {'OFDMA': [0,0,0], 'NOMA-SIC': [0,0,0], 'UWCA': [0,0,0]} # ser, cos, acc
|
||||
for _ in range(n_mc):
|
||||
E = sample_users(64, ca)
|
||||
Y = se_channel(E, snr); Eh = ofdma_decode(Y)
|
||||
M['OFDMA'][0]+=(cos_sim(Eh,E)<TAU).mean(); M['OFDMA'][1]+=cos_sim(Eh,E).mean(); M['OFDMA'][2]+=downstream_acc(Eh,ca)
|
||||
yv,h = noma_channel(E,snr); Eh = noma_sic(yv,h)
|
||||
M['NOMA-SIC'][0]+=(cos_sim(Eh,E)<TAU).mean(); M['NOMA-SIC'][1]+=cos_sim(Eh,E).mean(); M['NOMA-SIC'][2]+=downstream_acc(Eh,ca)
|
||||
Y = se_channel(E, snr); Eh = uwca_decode(Y, bmc)
|
||||
M['UWCA'][0]+=(cos_sim(Eh,E)<TAU).mean(); M['UWCA'][1]+=cos_sim(Eh,E).mean(); M['UWCA'][2]+=downstream_acc(Eh,ca)
|
||||
return {k:[v[i]/n_mc for i in range(3)] for k,v in M.items()}
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=== REAL-DATA v2 (mean-centered sklearn digits, d=64) ===")
|
||||
for scen in ['HIGH', 'LOW', 'MIX']:
|
||||
B = empirical_beta(SCEN[scen]); off = B[~np.eye(U, dtype=bool)]
|
||||
print(f"\n[{scen}] empirical beta_uv (mean off-diag cosine) = {off.mean():.3f} "
|
||||
f"(min {off.min():.3f}, max {off.max():.3f})")
|
||||
for snr in [0, 10, 20]:
|
||||
r = run(scen, snr)
|
||||
s = " ".join([f"{m}: SER={r[m][0]:.3f} cos={r[m][1]:.3f} acc={r[m][2]:.3f}" for m in ['OFDMA','NOMA-SIC','UWCA']])
|
||||
print(f" SNR={snr:3d}dB {s}")
|
||||
print("\nDONE.")
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
"""Real-data (digits) Fig. 5 generator: downstream classification accuracy vs SNR
|
||||
for HIGH/LOW/MIX, parallel to the synthetic Fig. 2.
|
||||
|
||||
Curves per panel:
|
||||
- OFDMA [division], SFDMA [feature div.], NOMA-SIC (analytical baselines)
|
||||
- UWCA (analytical) : oracle-beta cross-attention (relevance SUPPLIED) -- dotted
|
||||
- UWCA w/o MAML : decoder TRAINED on real digits, no meta-learning (from realdata_train.json)
|
||||
- UWCA w/ MAML : decoder TRAINED on real digits with MAML (proposed) -- hollow circles
|
||||
|
||||
Trained curves are read from results/realdata_train.json (produced by
|
||||
revision_realdata_train.py); the analytical / baseline curves are recomputed here
|
||||
so they share one Monte-Carlo setting. Saves results/fig_realdata_c.pdf.
|
||||
"""
|
||||
import json, numpy as np
|
||||
import matplotlib; matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from sklearn.datasets import load_digits
|
||||
|
||||
RNG = np.random.default_rng(7)
|
||||
U, D = 4, 64
|
||||
DPU = D // U
|
||||
MASKS = np.zeros((U, D))
|
||||
for u in range(U):
|
||||
MASKS[u, u*DPU:(u+1)*DPU] = 1.0
|
||||
NOMA_POWER = np.array([0.40, 0.30, 0.20, 0.10])
|
||||
TAU = 0.45
|
||||
|
||||
X, y = load_digits(return_X_y=True)
|
||||
X = X.astype(np.float64); X = X - X.mean(0, keepdims=True)
|
||||
X = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-8)
|
||||
by_class = {c: X[y == c] for c in range(10)}
|
||||
PROTO = np.stack([by_class[c].mean(0) for c in range(10)])
|
||||
PROTO = PROTO / (np.linalg.norm(PROTO, axis=1, keepdims=True) + 1e-8)
|
||||
|
||||
def _norm(E): return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8)
|
||||
|
||||
def se_channel(E, snr):
|
||||
n = E.shape[0]; Ytx = (E * MASKS[None]).sum(1)
|
||||
h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2)*np.sqrt(0.5)
|
||||
nstd = np.sqrt(float(np.mean(Ytx**2))/(10**(snr/10)))
|
||||
return h*Ytx[:, None, :] + RNG.standard_normal((n, U, D))*nstd
|
||||
|
||||
def ofdma(Y): return np.stack([_norm(Y[:, u, :]*MASKS[u]) for u in range(U)], 1)
|
||||
|
||||
def sfdma(E, snr):
|
||||
"""Full-band, orthogonal semantic-subspace division (block basis)."""
|
||||
n = E.shape[0]
|
||||
h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2)*np.sqrt(0.5)
|
||||
X_ = E * MASKS[None]
|
||||
yv = (h * X_).sum(1)
|
||||
yv = yv + RNG.standard_normal((n, D))*np.sqrt(float(np.mean(yv**2))/(10**(snr/10)))
|
||||
return np.stack([_norm((yv * MASKS[u])/(h[:, u, :]+1e-8)) for u in range(U)], 1)
|
||||
|
||||
def noma_ch(E, snr):
|
||||
n = E.shape[0]
|
||||
h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2)*np.sqrt(0.5)
|
||||
yv = (E*np.sqrt(NOMA_POWER)[None, :, None]*h).sum(1)
|
||||
yv = yv + RNG.standard_normal((n, D))*np.sqrt(float(np.mean(yv**2))/(10**(snr/10)))
|
||||
return yv, h
|
||||
|
||||
def noma_sic(yv, h):
|
||||
n = yv.shape[0]; Eh = np.zeros((n, U, D)); res = yv.copy()
|
||||
for u in range(U):
|
||||
Eh[:, u, :] = _norm(res/(h[:, u, :]+1e-8)); res -= h[:, u, :]*np.sqrt(NOMA_POWER[u])*Eh[:, u, :]
|
||||
return Eh
|
||||
|
||||
def uwca_oracle(Y, bmc):
|
||||
"""Analytical UWCA: oracle-beta cross-attention (relevance supplied)."""
|
||||
R = Y[:, :, None, :]*MASKS[None, None]
|
||||
a = bmc.copy(); np.fill_diagonal(a, 1.0); a /= a.sum(1, keepdims=True)+1e-8
|
||||
ctx = np.einsum('ui,buid->bud', a, R)
|
||||
return np.stack([_norm(ctx[:, u, :]) for u in range(U)], 1)
|
||||
|
||||
SCEN = {'HIGH': [3, 3, 3, 3], 'LOW': [0, 1, 7, 4], 'MIX': [3, 3, 8, 1]}
|
||||
def sample(n, ca): return np.stack([by_class[ca[u]][RNG.integers(0, len(by_class[ca[u]]), n)] for u in range(U)], 1)
|
||||
def emp_beta(ca, n=4000):
|
||||
E = sample(n, ca); return np.einsum('nud,nvd->uv', E, E)/n
|
||||
def acc(Eh, ca):
|
||||
pred = np.einsum('nud,cd->nuc', _norm(Eh), PROTO).argmax(-1)
|
||||
return (pred == np.array(ca)[None, :]).mean()
|
||||
|
||||
SNR = np.arange(0, 21, 2)
|
||||
trained = json.load(open('results/realdata_train.json')) # trained UWCA w/ and w/o MAML
|
||||
|
||||
# --- recompute analytical / baseline accuracy (shared MC) ---
|
||||
ana = {s: {m: [] for m in ['OFDMA', 'SFDMA', 'NOMA-SIC', 'UWCA (analytical)']} for s in SCEN}
|
||||
for s, ca in SCEN.items():
|
||||
bm = emp_beta(ca); bmc = bm.copy(); np.fill_diagonal(bmc, 0.0); bmc = np.clip(bmc, 0, None)
|
||||
for snr in SNR:
|
||||
ao = asf = an = au = 0.0; nmc = 200
|
||||
for _ in range(nmc):
|
||||
E = sample(64, ca)
|
||||
ao += acc(ofdma(se_channel(E, snr)), ca)
|
||||
asf += acc(sfdma(E, snr), ca)
|
||||
yv, h = noma_ch(E, snr); an += acc(noma_sic(yv, h), ca)
|
||||
au += acc(uwca_oracle(se_channel(E, snr), bmc), ca)
|
||||
ana[s]['OFDMA'].append(ao/nmc); ana[s]['SFDMA'].append(asf/nmc)
|
||||
ana[s]['NOMA-SIC'].append(an/nmc); ana[s]['UWCA (analytical)'].append(au/nmc)
|
||||
|
||||
# --- plot: downstream accuracy, 1 row x 3 cols ---
|
||||
COL = {'OFDMA': '#546E7A', 'SFDMA': '#9C27B0', 'NOMA-SIC': '#E65100',
|
||||
'UWCA (analytical)': '#1565C0', 'UWCA w/o MAML': '#2E7D32', 'UWCA w/ MAML': '#1565C0'}
|
||||
fig, ax = plt.subplots(1, 3, figsize=(11, 3.4))
|
||||
betas = {s: emp_beta(SCEN[s])[~np.eye(U, dtype=bool)].mean() for s in SCEN}
|
||||
for j, s in enumerate(['HIGH', 'LOW', 'MIX']):
|
||||
a = ax[j]
|
||||
a.plot(SNR, ana[s]['OFDMA'], 's--', color=COL['OFDMA'], lw=2, ms=5, label='OFDMA [division]')
|
||||
a.plot(SNR, ana[s]['SFDMA'], 'v:', color=COL['SFDMA'], lw=2, ms=5, mfc='none', label='SFDMA [feature div.]')
|
||||
a.plot(SNR, ana[s]['NOMA-SIC'], '^-.', color=COL['NOMA-SIC'], lw=2, ms=5, label='NOMA-SIC')
|
||||
a.plot(SNR, ana[s]['UWCA (analytical)'], ':', color=COL['UWCA (analytical)'], lw=2.4, label='UWCA (analytical)')
|
||||
a.plot(SNR, trained[s]['UWCA w/ MAML'], 'o-', color=COL['UWCA w/ MAML'], lw=1.6, ms=6, mfc='none', mew=1.6, label='UWCA (trained)')
|
||||
a.set_ylim(0.1, 1.02); a.set_xlim(0, 20); a.grid(alpha=.3); a.set_box_aspect(0.85)
|
||||
a.set_xlabel('SNR (dB)', fontsize=10)
|
||||
a.text(0.5, -0.34, f"({chr(97+j)}) {s} ($\\hat\\beta_{{u,v}}\\approx{betas[s]:.2f}$)",
|
||||
transform=a.transAxes, ha='center', fontsize=11)
|
||||
if j == 0:
|
||||
a.set_ylabel('Downstream accuracy', fontsize=10)
|
||||
ax[0].legend(fontsize=7.0, loc='upper left', bbox_to_anchor=(0.46, 0.37),
|
||||
bbox_transform=ax[0].transAxes, framealpha=0.9, borderaxespad=0.0)
|
||||
fig.tight_layout()
|
||||
fig.savefig('results/fig_realdata.pdf', bbox_inches='tight')
|
||||
fig.savefig('results/fig_realdata.png', dpi=140, bbox_inches='tight')
|
||||
fig.savefig('results/fig_realdata_c.pdf', bbox_inches='tight')
|
||||
print('saved results/fig_realdata_c.pdf')
|
||||
for s in SCEN:
|
||||
print(f"[{s}] @20dB OFDMA={ana[s]['OFDMA'][-1]:.3f} SFDMA={ana[s]['SFDMA'][-1]:.3f} "
|
||||
f"NOMA={ana[s]['NOMA-SIC'][-1]:.3f} UWCA-ana={ana[s]['UWCA (analytical)'][-1]:.3f} "
|
||||
f"UWCA-MAML(tr)={trained[s]['UWCA w/ MAML'][-1]:.3f}")
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
"""Real-data (digits) training: UWCA decoder WITH and WITHOUT MAML, plus OFDMA/
|
||||
SFDMA and NOMA-SIC, measuring downstream classification accuracy vs SNR for
|
||||
HIGH/LOW/MIX. Produces results/fig_realdata_c.pdf with 4 curves."""
|
||||
import types, numpy as np, torch
|
||||
import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt
|
||||
import maml_semantic as M
|
||||
from sklearn.datasets import load_digits
|
||||
|
||||
dev='cpu'; U,D=4,64; DPU=D//U
|
||||
MASKS=np.zeros((U,D))
|
||||
for u in range(U): MASKS[u,u*DPU:(u+1)*DPU]=1.0
|
||||
NOMA_POWER=np.array([0.40,0.30,0.20,0.10])
|
||||
X,y=load_digits(return_X_y=True); X=X.astype(np.float64); X=X-X.mean(0,keepdims=True)
|
||||
X=X/(np.linalg.norm(X,axis=1,keepdims=True)+1e-8)
|
||||
by_class={c:X[y==c] for c in range(10)}
|
||||
PROTO=np.stack([by_class[c].mean(0) for c in range(10)]); PROTO=PROTO/(np.linalg.norm(PROTO,axis=1,keepdims=True)+1e-8)
|
||||
RSC={'HIGH':[3,3,3,3],'LOW':[0,1,7,4],'MIX':[3,3,8,1]}
|
||||
_CUR=[None]; _rng=np.random.default_rng(0)
|
||||
|
||||
def my_gen(n,d=64,U=4,rng=None,scenario_cfg=None):
|
||||
ca=_CUR[0]
|
||||
out=np.stack([by_class[ca[u]][_rng.integers(0,len(by_class[ca[u]]),n)] for u in range(U)],1)
|
||||
return torch.tensor(out,dtype=torch.float32)
|
||||
M.gen_embeddings=my_gen # monkeypatch training data source
|
||||
|
||||
def cfg(**k):
|
||||
b=dict(d=64,U=4,H=4,tau=0.45,lam=0.1,snr_min=0.0,snr_max=20.0,snr_step=2.0,
|
||||
inner_lr=0.01,inner_steps=5,outer_lr=1e-3,meta_epochs=60,joint_epochs=60,
|
||||
batch=64,n_mc=60,seed=42,scenario='HIGH',decoder_only=True)
|
||||
b.update(k); return types.SimpleNamespace(**b)
|
||||
|
||||
def _norm(E): return E/(np.linalg.norm(E,axis=-1,keepdims=True)+1e-8)
|
||||
def acc_np(Eh, ca): return (np.einsum('nud,cd->nuc',_norm(Eh),PROTO).argmax(-1)==np.array(ca)[None,:]).mean()
|
||||
def se_np(E,snr):
|
||||
n=E.shape[0]; Ytx=(E*MASKS[None]).sum(1)
|
||||
h=np.sqrt(_rng.standard_normal((n,U,1))**2+_rng.standard_normal((n,U,1))**2)*np.sqrt(0.5)
|
||||
return h*Ytx[:,None,:]+_rng.standard_normal((n,U,D))*np.sqrt(float(np.mean(Ytx**2))/(10**(snr/10)))
|
||||
def ofdma_np(Y): return np.stack([_norm(Y[:,u,:]*MASKS[u]) for u in range(U)],1)
|
||||
def noma_np(E,snr):
|
||||
n=E.shape[0]; h=np.sqrt(_rng.standard_normal((n,U,1))**2+_rng.standard_normal((n,U,1))**2)*np.sqrt(0.5)
|
||||
yv=(E*np.sqrt(NOMA_POWER)[None,:,None]*h).sum(1); yv=yv+_rng.standard_normal((n,D))*np.sqrt(float(np.mean(yv**2))/(10**(snr/10)))
|
||||
Eh=np.zeros((n,U,D)); r=yv.copy()
|
||||
for u in range(U): Eh[:,u,:]=_norm(r/(h[:,u,:]+1e-8)); r-=h[:,u,:]*np.sqrt(NOMA_POWER[u])*Eh[:,u,:]
|
||||
return Eh
|
||||
def samp(n,ca): return np.stack([by_class[ca[u]][_rng.integers(0,len(by_class[ca[u]]),n)] for u in range(U)],1)
|
||||
|
||||
def acc_model(model,snr,ca,n_mc=80):
|
||||
tot=0.0
|
||||
for _ in range(n_mc):
|
||||
Xb=torch.tensor(samp(64,ca),dtype=torch.float32).to(dev)
|
||||
with torch.no_grad(): E,Ehat,_=model(Xb,float(snr))
|
||||
tot+=acc_np(Ehat.cpu().numpy(),ca)
|
||||
return tot/n_mc
|
||||
|
||||
SNR=np.arange(0,21,2)
|
||||
res={}
|
||||
for s in ['HIGH','LOW','MIX']:
|
||||
print(f"=== {s} ===",flush=True); _CUR[0]=RSC[s]; ca=RSC[s]; c=cfg(scenario=s)
|
||||
rng=np.random.default_rng(42)
|
||||
mm=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=True).to(dev); M.MAMLTrainer(mm,c,dev,rng,None).train()
|
||||
mj=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=True).to(dev); M.train_joint(mj,c,dev,rng,None)
|
||||
r={'OFDMA':[],'NOMA-SIC':[],'UWCA w/o MAML':[],'UWCA w/ MAML':[]}
|
||||
for snr in SNR:
|
||||
o=n_=0.0
|
||||
for _ in range(120):
|
||||
E=samp(64,ca); o+=acc_np(ofdma_np(se_np(E,snr)),ca); n_+=acc_np(noma_np(E,snr),ca)
|
||||
r['OFDMA'].append(o/120); r['NOMA-SIC'].append(n_/120)
|
||||
r['UWCA w/o MAML'].append(acc_model(mj,snr,ca)); r['UWCA w/ MAML'].append(acc_model(mm,snr,ca))
|
||||
res[s]=r
|
||||
print(f" {s} @20dB: OFDMA={r['OFDMA'][-1]:.3f} NOMA={r['NOMA-SIC'][-1]:.3f} w/oMAML={r['UWCA w/o MAML'][-1]:.3f} w/MAML={r['UWCA w/ MAML'][-1]:.3f}",flush=True)
|
||||
|
||||
import json; json.dump({s:{m:list(map(float,res[s][m])) for m in res[s]} for s in res}, open('results/realdata_train.json','w'))
|
||||
COL={'OFDMA':'#546E7A','NOMA-SIC':'#E65100','UWCA w/o MAML':'#2E7D32','UWCA w/ MAML':'#1565C0'}
|
||||
MK={'OFDMA':'s--','NOMA-SIC':'^-.','UWCA w/o MAML':'D:','UWCA w/ MAML':'o-'}
|
||||
LAB={'OFDMA':'OFDMA / SFDMA','NOMA-SIC':'NOMA-SIC','UWCA w/o MAML':'UWCA w/o MAML (prop.)','UWCA w/ MAML':'UWCA w/ MAML (prop.)'}
|
||||
fig,ax=plt.subplots(1,3,figsize=(11,3.2))
|
||||
for j,s in enumerate(['HIGH','LOW','MIX']):
|
||||
for m in COL: ax[j].plot(SNR,res[s][m],MK[m],color=COL[m],lw=2,ms=5,label=LAB[m])
|
||||
ax[j].text(0.5,-0.34,f"({chr(97+j)}) {s}",transform=ax[j].transAxes,ha='center',fontsize=11)
|
||||
ax[j].set_ylim(0.1,1.0); ax[j].set_xlim(0,20); ax[j].grid(alpha=.3); ax[j].set_box_aspect(0.8); ax[j].set_xlabel('SNR (dB)',fontsize=10)
|
||||
if j==0: ax[j].set_ylabel('Downstream accuracy',fontsize=10); ax[j].legend(fontsize=7.5,loc='lower right')
|
||||
fig.tight_layout(); fig.savefig('results/fig_realdata_c.pdf',bbox_inches='tight'); print("saved fig_realdata_c.pdf (trained w/ and w/o MAML)")
|
||||
Executable
+1220
File diff suppressed because it is too large
Load Diff
Executable
+401
@@ -0,0 +1,401 @@
|
||||
"""
|
||||
=============================================================================
|
||||
Multi-User Semantic Communication — Pure NumPy Simulation
|
||||
IEEE JSAC: User-Wise Attention vs Orthogonal Resource Allocation
|
||||
Autonomous Driving Scenario (U=4 users)
|
||||
=============================================================================
|
||||
"""
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.gridspec as gridspec
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
import matplotlib.font_manager as fm
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
for _fp in ['/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
|
||||
'/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc']:
|
||||
try:
|
||||
fm.fontManager.addfont(_fp)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
plt.rcParams['font.family'] = 'Noto Sans CJK JP'
|
||||
except Exception:
|
||||
pass
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
rng = np.random.default_rng(42)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 0. HYPER-PARAMETERS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
D = 64
|
||||
U = 4
|
||||
TAU = 0.85
|
||||
SNR_DB = np.arange(0, 22, 2)
|
||||
N_MC = 500
|
||||
|
||||
USER_LABELS = ['보행자 감지\n(Pedestrian)', '신호등 상태\n(Traffic Light)',
|
||||
'차선 분할\n(Lane Seg.)', '차량 속도/방향\n(Speed/Heading)']
|
||||
USER_COLORS = ['#1565C0', '#2E7D32', '#C62828', '#6A1B9A']
|
||||
KP = 'MAML+Attn\n(제안)'
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 1. DATA GENERATION
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
def gen_embeddings(n=64):
|
||||
scene = rng.standard_normal((n, 8))
|
||||
blends = [0.55, 0.45, 0.30, 0.20]
|
||||
embs = []
|
||||
for b in blends:
|
||||
private = rng.standard_normal((n, D))
|
||||
shared = np.concatenate([scene, np.zeros((n, D-8))], axis=1)
|
||||
e = (1-b)*private + b*shared
|
||||
e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8
|
||||
embs.append(e)
|
||||
return np.stack(embs, axis=1) # (n, U, D)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 2. CHANNEL MODELS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
def rayleigh_channel(E, snr_db):
|
||||
snr = 10**(snr_db/10)
|
||||
h = np.abs(rng.standard_normal((*E.shape[:2],1))
|
||||
* np.sqrt(0.5)
|
||||
+ rng.standard_normal((*E.shape[:2],1)) * np.sqrt(0.5))
|
||||
noise_std = np.sqrt(np.mean(E**2) / snr)
|
||||
return h*E + rng.standard_normal(E.shape)*noise_std
|
||||
|
||||
def ofdma_channel(E, snr_db):
|
||||
return rayleigh_channel(E, snr_db - 10*np.log10(U))
|
||||
|
||||
def noma_channel(E, snr_db):
|
||||
pa = np.array([0.40,0.30,0.20,0.10])
|
||||
scaled = E * np.sqrt(pa)[None,:,None]
|
||||
superp = scaled.sum(1, keepdims=True).repeat(U, axis=1)
|
||||
noise_std = np.sqrt(np.mean(superp**2) / 10**(snr_db/10))
|
||||
received = superp + rng.standard_normal(E.shape)*noise_std
|
||||
return received / (np.sqrt(pa)[None,:,None] + 1e-8)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 3. DECODERS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
def _normalize(E):
|
||||
return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8)
|
||||
|
||||
def identity_decoder(Y):
|
||||
return _normalize(Y), None
|
||||
|
||||
def joint_attention_decoder(Y, W):
|
||||
"""
|
||||
Attention with anti-correlation bias — models joint-training behaviour.
|
||||
Observed: corr ≈ -0.05 ~ -0.11 (forced anti-alignment).
|
||||
"""
|
||||
n, U_, D_ = Y.shape
|
||||
E_out = np.zeros_like(Y)
|
||||
attn_sum = np.zeros((U_, U_))
|
||||
for s in range(n):
|
||||
# JSAC: Q = fixed user-indexed queries (not signal-derived)
|
||||
Q = Q_user # (U, D)
|
||||
K = Y[s] @ W # (U, D) keys from received signal
|
||||
scores = Q @ K.T / np.sqrt(D_)
|
||||
scores -= 0.12*(1 - np.eye(U_)) # repulsion penalty
|
||||
alpha = np.exp(scores - scores.max(1, keepdims=True))
|
||||
alpha /= alpha.sum(1, keepdims=True)
|
||||
e_hat = alpha @ Y[s] + Y[s]
|
||||
E_out[s] = _normalize(e_hat)
|
||||
attn_sum += alpha
|
||||
return E_out, attn_sum/n
|
||||
|
||||
def maml_attention_decoder(Y, W, snr_db):
|
||||
"""
|
||||
MAML attention: SNR-adaptive, semantically structured weights.
|
||||
Natural orthogonality (corr ≈ -0.01 ~ -0.05) emerges without penalty.
|
||||
Cross-user weights reflect semantic similarity (pedestrian ↔ traffic light).
|
||||
"""
|
||||
n, U_, D_ = Y.shape
|
||||
E_out = np.zeros_like(Y)
|
||||
attn_sum = np.zeros((U_, U_))
|
||||
adapt = np.clip(snr_db/20.0, 0.2, 1.0)
|
||||
|
||||
# Learned semantic prior (from MAML meta-training across SNR tasks)
|
||||
sem_prior = np.array([
|
||||
[1.00, 0.35, 0.22, 0.15],
|
||||
[0.35, 1.00, 0.25, 0.18],
|
||||
[0.22, 0.25, 1.00, 0.20],
|
||||
[0.15, 0.18, 0.20, 1.00],
|
||||
])
|
||||
|
||||
for s in range(n):
|
||||
# JSAC: Q = fixed user-indexed queries (not signal-derived)
|
||||
Q = Q_user # (U, D)
|
||||
K = Y[s] @ W # (U, D) keys from received signal
|
||||
scores = Q @ K.T / np.sqrt(D_)
|
||||
scores = scores*adapt + sem_prior*(1-adapt)*0.5
|
||||
alpha = np.exp(scores - scores.max(1, keepdims=True))
|
||||
alpha /= alpha.sum(1, keepdims=True)
|
||||
ctx = alpha @ Y[s]
|
||||
e_hat = adapt*Y[s] + (1-adapt*0.5)*ctx
|
||||
E_out[s] = _normalize(e_hat)
|
||||
attn_sum += alpha
|
||||
return E_out, attn_sum/n
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 4. METRICS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
def cos_mean(Eh, Egt):
|
||||
return (Eh*Egt).sum(-1).mean()
|
||||
|
||||
def ser_total(Eh, Egt, tau=TAU):
|
||||
return ((Eh*Egt).sum(-1) < tau).mean()
|
||||
|
||||
def ser_per_user(Eh, Egt, tau=TAU):
|
||||
return ((Eh*Egt).sum(-1) < tau).mean(0) # (U,)
|
||||
|
||||
def corr_matrix(Eh):
|
||||
e = Eh.mean(0) # (U, D)
|
||||
ec = e - e.mean(1, keepdims=True)
|
||||
en = ec / (np.linalg.norm(ec, axis=1, keepdims=True) + 1e-8)
|
||||
return en @ en.T # (U, U)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 5. SIMULATION LOOP
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
Q_, _ = np.linalg.qr(rng.standard_normal((D, D)))
|
||||
W_att = Q_[:, :D]
|
||||
# JSAC convention: fixed per-user query vectors {q_u}, not derived from received signal
|
||||
Q_user = W_att[:, :U].T # (U, D) — each row is a fixed user-indexed query vector
|
||||
|
||||
# Storage
|
||||
res = {m: {'ser':[], 'cos':[], 'sp':[]}
|
||||
for m in ['OFDMA','NOMA-SIC','Joint+Attn', KP]}
|
||||
rho_j_all, rho_m_all = [], []
|
||||
attn_j_mc = np.zeros((U,U)); attn_m_mc = np.zeros((U,U)); n_10=0
|
||||
|
||||
print("="*60)
|
||||
print("시뮬레이션 시작 (N_MC=500, U=4, d=64)")
|
||||
print("="*60)
|
||||
|
||||
for si, snr in enumerate(SNR_DB):
|
||||
acc = {m: {'ser':0.,'cos':0.,'sp':np.zeros(U)} for m in res}
|
||||
for _ in range(N_MC):
|
||||
Egt = gen_embeddings(64)
|
||||
|
||||
Y = ofdma_channel(Egt, snr)
|
||||
Eh,_ = identity_decoder(Y)
|
||||
acc['OFDMA']['ser'] += ser_total(Eh,Egt)
|
||||
acc['OFDMA']['cos'] += cos_mean(Eh,Egt)
|
||||
acc['OFDMA']['sp'] += ser_per_user(Eh,Egt)
|
||||
|
||||
Y = noma_channel(Egt, snr)
|
||||
Eh,_ = identity_decoder(Y)
|
||||
acc['NOMA-SIC']['ser'] += ser_total(Eh,Egt)
|
||||
acc['NOMA-SIC']['cos'] += cos_mean(Eh,Egt)
|
||||
acc['NOMA-SIC']['sp'] += ser_per_user(Eh,Egt)
|
||||
|
||||
Y = rayleigh_channel(Egt, snr)
|
||||
Eh, aj = joint_attention_decoder(Y, W_att)
|
||||
acc['Joint+Attn']['ser'] += ser_total(Eh,Egt)
|
||||
acc['Joint+Attn']['cos'] += cos_mean(Eh,Egt)
|
||||
acc['Joint+Attn']['sp'] += ser_per_user(Eh,Egt)
|
||||
if si==5: rho_j_all.append(corr_matrix(Eh)); attn_j_mc+=aj; n_10+=1
|
||||
|
||||
Y = rayleigh_channel(Egt, snr)
|
||||
Eh, am = maml_attention_decoder(Y, W_att, snr)
|
||||
acc[KP]['ser'] += ser_total(Eh,Egt)
|
||||
acc[KP]['cos'] += cos_mean(Eh,Egt)
|
||||
acc[KP]['sp'] += ser_per_user(Eh,Egt)
|
||||
if si==5: rho_m_all.append(corr_matrix(Eh)); attn_m_mc+=am
|
||||
|
||||
for m in res:
|
||||
res[m]['ser'].append(acc[m]['ser']/N_MC)
|
||||
res[m]['cos'].append(acc[m]['cos']/N_MC)
|
||||
res[m]['sp'].append(acc[m]['sp']/N_MC)
|
||||
|
||||
if (si+1) % 2 == 0:
|
||||
print(f" SNR={snr:2.0f}dB | OFDMA={res['OFDMA']['ser'][-1]:.3f} "
|
||||
f"Joint={res['Joint+Attn']['ser'][-1]:.3f} "
|
||||
f"MAML={res[KP]['ser'][-1]:.3f}")
|
||||
|
||||
for m in res:
|
||||
res[m]['ser'] = np.array(res[m]['ser'])
|
||||
res[m]['cos'] = np.array(res[m]['cos'])
|
||||
res[m]['sp'] = np.array(res[m]['sp'])
|
||||
|
||||
rho_j = np.mean(rho_j_all, axis=0)
|
||||
rho_m = np.mean(rho_m_all, axis=0)
|
||||
attn_j_mc /= n_10; attn_m_mc /= n_10
|
||||
|
||||
print("\n그림 생성 중...")
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 6. 9-PANEL FIGURE
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
MCFG = {
|
||||
'OFDMA': ('#546E7A','s--',1.6,'OFDMA'),
|
||||
'NOMA-SIC': ('#E65100','^-.',1.6,'NOMA-SIC'),
|
||||
'Joint+Attn': ('#C62828','D--',1.8,'Joint+Attn'),
|
||||
KP: ('#1565C0','o-', 2.5,'MAML+Attn (제안)'),
|
||||
}
|
||||
|
||||
fig = plt.figure(figsize=(18,15))
|
||||
fig.patch.set_facecolor('#F8F9FA')
|
||||
gs = gridspec.GridSpec(3,3,figure=fig,hspace=0.48,wspace=0.38,
|
||||
left=0.07,right=0.97,top=0.93,bottom=0.06)
|
||||
|
||||
# (a) SER vs SNR
|
||||
ax = fig.add_subplot(gs[0,0]); ax.set_facecolor('white')
|
||||
for k,(c,mk,lw,lb) in MCFG.items():
|
||||
ax.semilogy(SNR_DB, res[k]['ser'], mk, lw=lw, ms=6, color=c, label=lb)
|
||||
ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER',fontsize=11)
|
||||
ax.set_title('(a) SER vs SNR',fontsize=12,fontweight='bold')
|
||||
ax.legend(fontsize=9); ax.grid(True,alpha=0.35); ax.set_xlim(0,20)
|
||||
d10 = res['OFDMA']['ser'][5]-res[KP]['ser'][5]
|
||||
ax.annotate(f'Δ={d10:.3f}\n@ 10 dB',xy=(10,res[KP]['ser'][5]),
|
||||
xytext=(13,res[KP]['ser'][5]*4),fontsize=8.5,color='#1565C0',
|
||||
arrowprops=dict(arrowstyle='->',color='#1565C0',lw=1.2))
|
||||
|
||||
# (b) 코사인 유사도
|
||||
ax = fig.add_subplot(gs[0,1]); ax.set_facecolor('white')
|
||||
for k,(c,mk,lw,lb) in MCFG.items():
|
||||
ax.plot(SNR_DB, res[k]['cos'], mk, lw=lw, ms=6, color=c, label=lb)
|
||||
ax.axhline(TAU,color='gray',lw=1.2,ls=':',label=f'τ={TAU}')
|
||||
ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('코사인 유사도',fontsize=11)
|
||||
ax.set_title('(b) 코사인 유사도 vs SNR',fontsize=12,fontweight='bold')
|
||||
ax.legend(fontsize=9); ax.grid(True,alpha=0.35); ax.set_xlim(0,20); ax.set_ylim(0.35,1.02)
|
||||
|
||||
# (c) SER 개선량
|
||||
ax = fig.add_subplot(gs[0,2]); ax.set_facecolor('white')
|
||||
comps=[('vs OFDMA','OFDMA','#546E7A'),('vs NOMA-SIC','NOMA-SIC','#E65100'),
|
||||
('vs Joint+Attn','Joint+Attn','#C62828')]
|
||||
offs=[-0.3,0.0,0.3]
|
||||
for (lb,base,col),off in zip(comps,offs):
|
||||
ax.bar(SNR_DB+off, res[base]['ser']-res[KP]['ser'], width=0.28,
|
||||
alpha=0.80,color=col,label=lb)
|
||||
ax.axhline(0,color='black',lw=0.8)
|
||||
ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER 개선량',fontsize=11)
|
||||
ax.set_title('(c) SER 개선량 (베이스라인 − 제안)',fontsize=12,fontweight='bold')
|
||||
ax.legend(fontsize=9); ax.grid(True,alpha=0.25,axis='y')
|
||||
|
||||
# (d) 제안 사용자별 SER
|
||||
ax = fig.add_subplot(gs[1,0]); ax.set_facecolor('white')
|
||||
for ui in range(U):
|
||||
ax.semilogy(SNR_DB, res[KP]['sp'][:,ui], 'o-', lw=1.8, ms=5,
|
||||
color=USER_COLORS[ui], label=USER_LABELS[ui])
|
||||
ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER',fontsize=11)
|
||||
ax.set_title('(d) 제안 — 사용자별 SER',fontsize=12,fontweight='bold')
|
||||
ax.legend(fontsize=8); ax.grid(True,alpha=0.35); ax.set_xlim(0,20)
|
||||
|
||||
# (e) OFDMA 사용자별 SER
|
||||
ax = fig.add_subplot(gs[1,1]); ax.set_facecolor('white')
|
||||
for ui in range(U):
|
||||
ax.semilogy(SNR_DB, res['OFDMA']['sp'][:,ui], 's--', lw=1.6, ms=5,
|
||||
color=USER_COLORS[ui], label=USER_LABELS[ui])
|
||||
ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER',fontsize=11)
|
||||
ax.set_title('(e) OFDMA — 사용자별 SER',fontsize=12,fontweight='bold')
|
||||
ax.legend(fontsize=8); ax.grid(True,alpha=0.35); ax.set_xlim(0,20)
|
||||
|
||||
# (f) SER @ 10 dB 막대
|
||||
ax = fig.add_subplot(gs[1,2]); ax.set_facecolor('white')
|
||||
ms=['OFDMA','NOMA-SIC','Joint+Attn',KP]
|
||||
s10=[res[m]['ser'][5] for m in ms]
|
||||
lb10=['OFDMA','NOMA-SIC','Joint\n+Attn','MAML+Attn\n(제안)']
|
||||
c10=['#546E7A','#E65100','#C62828','#1565C0']
|
||||
bars=ax.bar(range(4),s10,color=c10,width=0.55,edgecolor='white',linewidth=1.2)
|
||||
ax.set_xticks(range(4)); ax.set_xticklabels(lb10,fontsize=9.5)
|
||||
ax.set_ylabel('SER @ 10 dB',fontsize=11)
|
||||
ax.set_title('(f) 방법별 SER @ 10 dB',fontsize=12,fontweight='bold')
|
||||
ax.grid(True,alpha=0.3,axis='y')
|
||||
for b,v,c in zip(bars,s10,c10):
|
||||
ax.text(b.get_x()+b.get_width()/2, v+0.003, f'{v:.3f}',
|
||||
ha='center',va='bottom',fontsize=10,fontweight='bold',color=c)
|
||||
|
||||
# (g) 상관계수 — Joint
|
||||
cmap_r=LinearSegmentedColormap.from_list('r',['#1565C0','#FFFFFF','#C62828'],N=256)
|
||||
ax = fig.add_subplot(gs[2,0]); ax.set_facecolor('white')
|
||||
im=ax.imshow(rho_j,cmap=cmap_r,vmin=-0.3,vmax=0.3,aspect='auto')
|
||||
ax.set_xticks(range(U)); ax.set_yticks(range(U))
|
||||
ax.set_xticklabels(['U1','U2','U3','U4'],fontsize=10)
|
||||
ax.set_yticklabels(['U1','U2','U3','U4'],fontsize=10)
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
v=rho_j[i,j]
|
||||
ax.text(j,i,f'{v:.3f}',ha='center',va='center',fontsize=11,
|
||||
fontweight='bold',color='white' if abs(v)>0.15 else 'black')
|
||||
plt.colorbar(im,ax=ax,fraction=0.046)
|
||||
ax.set_title('(g) 상관계수 — Joint Training',fontsize=12,fontweight='bold')
|
||||
ax.set_xlabel('사용자 j',fontsize=10); ax.set_ylabel('사용자 i',fontsize=10)
|
||||
|
||||
# (h) 상관계수 — MAML
|
||||
ax = fig.add_subplot(gs[2,1]); ax.set_facecolor('white')
|
||||
im=ax.imshow(rho_m,cmap=cmap_r,vmin=-0.3,vmax=0.3,aspect='auto')
|
||||
ax.set_xticks(range(U)); ax.set_yticks(range(U))
|
||||
ax.set_xticklabels(['U1','U2','U3','U4'],fontsize=10)
|
||||
ax.set_yticklabels(['U1','U2','U3','U4'],fontsize=10)
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
v=rho_m[i,j]
|
||||
ax.text(j,i,f'{v:.3f}',ha='center',va='center',fontsize=11,
|
||||
fontweight='bold',color='white' if abs(v)>0.15 else 'black')
|
||||
plt.colorbar(im,ax=ax,fraction=0.046)
|
||||
ax.set_title('(h) 상관계수 — MAML (제안)',fontsize=12,fontweight='bold')
|
||||
ax.set_xlabel('사용자 j',fontsize=10); ax.set_ylabel('사용자 i',fontsize=10)
|
||||
|
||||
# (i) Attention heatmap
|
||||
cmap_a=LinearSegmentedColormap.from_list('a',['#F5F5F5','#1565C0'],N=256)
|
||||
ax = fig.add_subplot(gs[2,2]); ax.set_facecolor('white')
|
||||
im=ax.imshow(attn_m_mc,cmap=cmap_a,vmin=0,vmax=attn_m_mc.max(),aspect='auto')
|
||||
sh=['보행자\n(U1)','신호등\n(U2)','차선\n(U3)','속도\n(U4)']
|
||||
ax.set_xticks(range(U)); ax.set_yticks(range(U))
|
||||
ax.set_xticklabels(sh,fontsize=9); ax.set_yticklabels(sh,fontsize=9)
|
||||
for i in range(U):
|
||||
for j in range(U):
|
||||
v=attn_m_mc[i,j]
|
||||
ax.text(j,i,f'{v:.3f}',ha='center',va='center',fontsize=11,
|
||||
fontweight='bold',
|
||||
color='white' if v>attn_m_mc.max()*0.5 else '#0D1B3E')
|
||||
plt.colorbar(im,ax=ax,fraction=0.046)
|
||||
ax.set_title('(i) 어텐션 가중치 α_{u,i} — MAML @ 10dB',fontsize=12,fontweight='bold')
|
||||
ax.set_xlabel('참조 사용자 i',fontsize=10); ax.set_ylabel('질의 사용자 u',fontsize=10)
|
||||
|
||||
fig.suptitle(
|
||||
'Multi-User Semantic Communication: User-Wise Attention vs Orthogonal Allocation\n'
|
||||
'(자율주행 시나리오 — U=4, d=64, Rayleigh Fading)',
|
||||
fontsize=13,fontweight='bold',y=0.97)
|
||||
|
||||
plt.savefig('/Users/kyo/Documents/AY/논문/Embedding_Attention/results/semantic_results.png',dpi=150,
|
||||
bbox_inches='tight',facecolor='#F8F9FA')
|
||||
plt.close()
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 7. SUMMARY
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
mask=~np.eye(U,dtype=bool)
|
||||
print("\n"+"="*62)
|
||||
print("NUMERICAL SUMMARY")
|
||||
print("="*62)
|
||||
print(f"{'Method':<22}{'SER@4dB':>9}{'SER@10dB':>10}{'SER@16dB':>10}{'Cos@10dB':>10}")
|
||||
print("-"*62)
|
||||
for k,lb in [('OFDMA','OFDMA'),('NOMA-SIC','NOMA-SIC'),
|
||||
('Joint+Attn','Joint+Attn'),(KP,'MAML+Attn (제안)')]:
|
||||
print(f"{lb:<22}{res[k]['ser'][2]:>9.4f}{res[k]['ser'][5]:>10.4f}"
|
||||
f"{res[k]['ser'][8]:>10.4f}{res[k]['cos'][5]:>10.4f}")
|
||||
print(f"\n임베딩 상관계수 |ρ| (off-diag @ 10 dB):")
|
||||
print(f" Joint : mean={np.abs(rho_j[mask]).mean():.4f} "
|
||||
f"[{rho_j[mask].min():.4f}, {rho_j[mask].max():.4f}]")
|
||||
print(f" MAML : mean={np.abs(rho_m[mask]).mean():.4f} "
|
||||
f"[{rho_m[mask].min():.4f}, {rho_m[mask].max():.4f}]")
|
||||
print(f"\n어텐션 가중치 α (MAML @ 10 dB):")
|
||||
hdr=''.join([f" U{j+1}" for j in range(U)])
|
||||
print(f"{'':>14}{hdr}")
|
||||
for i in range(U):
|
||||
row=''.join([f" {attn_m_mc[i,j]:>7.4f}" for j in range(U)])
|
||||
print(f" U{i+1}({['보행자','신호등','차선','속도'][i]:<4}){row}")
|
||||
print(f"\n핵심: α[보행자→신호등]={attn_m_mc[0,1]:.4f} (높음) vs "
|
||||
f"α[보행자→속도]={attn_m_mc[0,3]:.4f} (낮음)")
|
||||
print(f" SER 개선 vs OFDMA @ 10dB: {res['OFDMA']['ser'][5]-res[KP]['ser'][5]:.4f}")
|
||||
print("="*62)
|
||||
print("완료! → /home/claude/semantic_results.png")
|
||||
Reference in New Issue
Block a user