{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12139340,"sourceType":"datasetVersion","datasetId":7645099},{"sourceId":12293285,"sourceType":"datasetVersion","datasetId":7748073},{"sourceId":12328761,"sourceType":"datasetVersion","datasetId":7771623},{"sourceId":12351575,"sourceType":"datasetVersion","datasetId":7786958,"isSourceIdPinned":true},{"sourceId":12371910,"sourceType":"datasetVersion","datasetId":7800766},{"sourceId":12449409,"sourceType":"datasetVersion","datasetId":7834173},{"sourceId":242954653,"sourceType":"kernelVersion"},{"sourceId":246893721,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport json\nimport joblib\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport warnings\nimport random\nimport math\nimport polars as pl\nimport tensorflow as tf\nfrom tqdm.auto import tqdm\n\n# Keras / TensorFlow\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras.models import Model, load_model\nfrom tensorflow.keras.layers import (\n    Input, Conv1D, BatchNormalization, Activation, add, MaxPooling1D, Dropout,\n    Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Multiply, Reshape,\n    Lambda, Concatenate, LayerNormalization, MultiHeadAttention, GaussianNoise, TFSMLayer\n)\nfrom tensorflow.keras.optimizers import Adam as AdamTF\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.utils import Sequence, to_categorical, pad_sequences\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom tensorflow.keras.optimizers.schedules import CosineDecay\n\n# PyTorch\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# Scikit-learn & Scipy\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom scipy.spatial.transform import Rotation as R\nfrom scipy.signal import firwin\nfrom scipy.stats import rankdata\n\n# 評価メトリクス\ntry:\n    from cmi_2025_metric_copy_for_import import CompetitionMetric\nexcept ImportError:\n    CompetitionMetric = None\n    print(\"CompetitionMetric could not be imported. OOF/CV score will not be calculated.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:07:56.428973Z","iopub.execute_input":"2025-07-09T13:07:56.429415Z","iopub.status.idle":"2025-07-09T13:08:16.410066Z","shell.execute_reply.started":"2025-07-09T13:07:56.429397Z","shell.execute_reply":"2025-07-09T13:08:16.409358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed=42):\n    \"\"\"実行環境の乱数シードを統一的に設定する関数\"\"\"\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n    os.environ['TF_CUDNN_DETERMINISTIC'] = '1'\n    os.environ['TF_DETERMINISTIC_OPS'] = '1'\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(seed=42)\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:08:16.411798Z","iopub.execute_input":"2025-07-09T13:08:16.412377Z","iopub.status.idle":"2025-07-09T13:08:16.424126Z","shell.execute_reply.started":"2025-07-09T13:08:16.412351Z","shell.execute_reply":"2025-07-09T13:08:16.423519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN = True\n\n# --- パス設定 ---\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\n# YOUR_MODELS_DIRは自分の学習済みモデルが格納されているKaggleデータセットのパスに設定してください\nYOUR_MODELS_DIR = Path(\"/kaggle/input/gru-hybrid-ensemble\") # ★★★ 自分のモデルパスに変更 ★★★\nPUBLIC_TF_MODEL_DIR = Path(\"/kaggle/input/lb-0-78-quaternions-tf-bilstm-gru-attention\")\nPUBLIC_PT_MODEL_DIR = Path(\"/kaggle/input/cmi3-models-p\")\nEXPORT_DIR = Path(\"./\") # 学習済みモデルやアーティファクトの保存先\n\n# --- モデル学習ハイパーパラメータ ---\nBATCH_SIZE = 64          # バッチサイズ\nPAD_PERCENTILE = 95      # シーケンス長のパディングを決めるためのパーセンタイル値\nLR_INIT = 5e-4           # 学習率の初期値 (微調整)\nWD = 3e-3                # Weight Decay（L2正則化）の係数\nMIXUP_ALPHA = 0.4        # Mixupのα値\nEPOCHS = 200             # 最大エポック数 (増加)\nPATIENCE = 60            # EarlyStoppingのpatience (増加)\nN_SPLITS = 5             # クロスバリデーションの分割数\nMASKING_PROB = 0.25      # 学習時にTOF/THMデータをマスクする確率\nGATE_LOSS_WEIGHT = 0.2   # Gatedモデルのゲート損失に対する重み\n\nprint(f\"▶ ライブラリのインポート完了\")\nprint(f\"  - TensorFlow: {tf.__version__}\")\nprint(f\"  - PyTorch: {torch.__version__}\")\nprint(f\"▶ TRAINモード: {TRAIN}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:08:16.425006Z","iopub.execute_input":"2025-07-09T13:08:16.425255Z","iopub.status.idle":"2025-07-09T13:08:16.458593Z","shell.execute_reply.started":"2025-07-09T13:08:16.425233Z","shell.execute_reply":"2025-07-09T13:08:16.457923Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# # PyTorchモデル用の標準化パラメータ\n# mean_pt = torch.tensor([\n#     0, 0, 0, 0, 0, 0, 9.0319e-03, 1.0849e+00, -2.6186e-03, 3.7651e-03,\n#     -5.3660e-03, -2.8177e-03, 1.3318e-03, -1.5876e-04, 6.3495e-01,\n#     6.2877e-01, 6.0607e-01, 6.2142e-01, 6.3808e-01, 6.5420e-01,\n#     7.4102e-03, -3.4159e-03, -7.5237e-03, -2.6034e-02, 2.9704e-02,\n#     -3.1546e-02, -2.0610e-03, -4.6986e-03, -4.7216e-03, -2.6281e-02,\n#     1.5799e-02, 1.0016e-02\n# ], dtype=torch.float32).view(1, -1, 1).to(device)\n\n# std_pt = torch.tensor([\n#     1, 1, 1, 1, 1, 1, 0.2067, 0.8583, 0.3162,\n#     0.2668, 0.2917, 0.2341, 0.3023, 0.3281, 1.0264, 0.8838, 0.8686, 1.0973,\n#     1.0267, 0.9018, 0.4658, 0.2009, 0.2057, 1.2240, 0.9535, 0.6655, 0.2941,\n#     0.3421, 0.8156, 0.6565, 1.1034, 1.5577\n# ], dtype=torch.float32).view(1, -1, 1).to(device) + 1e-8","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:08:16.459661Z","iopub.execute_input":"2025-07-09T13:08:16.459944Z","iopub.status.idle":"2025-07-09T13:08:16.806666Z","shell.execute_reply.started":"2025-07-09T13:08:16.459919Z","shell.execute_reply":"2025-07-09T13:08:16.805963Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nclass ImuFeatureExtractorPT(nn.Module):\n    def __init__(self,fs=100.,**kwargs):super().__init__();self.fs=fs;k=15;self.lpf=nn.Conv1d(6,6,k,padding=k//2,groups=6,bias=False);nn.init.kaiming_uniform_(self.lpf.weight,a=math.sqrt(5));self.lpf_acc=nn.Conv1d(3,3,k,padding=k//2,groups=3,bias=False);self.lpf_gyro=nn.Conv1d(3,3,k,padding=k//2,groups=3,bias=False)\n    def forward(self,imu):acc,gyro=imu[:,:3,:],imu[:,3:6,:];acc_mag,gyro_mag=torch.norm(acc,dim=1,keepdim=True),torch.norm(gyro,dim=1,keepdim=True);jerk,gyro_delta=F.pad(acc[:,:,1:]-acc[:,:,:-1],(1,0)),F.pad(gyro[:,:,1:]-gyro[:,:,:-1],(1,0));acc_pow,gyro_pow=acc**2,gyro**2;acc_lpf,acc_hpf=self.lpf_acc(acc),acc-self.lpf_acc(acc);gyro_lpf,gyro_hpf=self.lpf_gyro(gyro),gyro-self.lpf_gyro(gyro);return torch.cat([acc,gyro,acc_mag,gyro_mag,jerk,gyro_delta,acc_pow,gyro_pow,acc_lpf,acc_hpf,gyro_lpf,gyro_hpf],dim=1)\nclass SEBlockPT(nn.Module):\n    def __init__(self,c,r=8):super().__init__();self.squeeze=nn.AdaptiveAvgPool1d(1);self.excitation=nn.Sequential(nn.Linear(c,c//r,bias=False),nn.ReLU(inplace=True),nn.Linear(c//r,c,bias=False),nn.Sigmoid())\n    def forward(self,x):b,c,_=x.size();y=self.squeeze(x).view(b,c);y=self.excitation(y).view(b,c,1);return x*y.expand_as(x)\nclass ResidualSECNNBlockPT(nn.Module):\n    def __init__(self,i,o,k,p=2,d=0.3):super().__init__();self.c1,self.b1=nn.Conv1d(i,o,k,padding=k//2,bias=False),nn.BatchNorm1d(o);self.c2,self.b2=nn.Conv1d(o,o,k,padding=k//2,bias=False),nn.BatchNorm1d(o);self.se=SEBlockPT(o);self.s=nn.Sequential(nn.Conv1d(i,o,1,bias=False),nn.BatchNorm1d(o))if i!=o else nn.Identity();self.p,self.d=nn.MaxPool1d(p),nn.Dropout(d)\n    def forward(self,x):o=F.relu(self.b1(self.c1(x)));o=self.b2(self.c2(o));o=self.se(o)+self.s(x);return self.d(self.p(F.relu(o)))\nclass PublicTwoBranchModel(nn.Module):\n    def __init__(self,p,i,t,n,d=[0.3]*4+[0.4,0.5,0.3],f=True,**kwargs):\n        super().__init__();imu_dim=32 if f else i;self.imu_fe=ImuFeatureExtractorPT(**kwargs)if f else nn.Identity();self.fn=7;fk=torch.tensor(firwin(33,1.0,fs=10.0,pass_zero=False),dtype=torch.float32).view(1,1,-1).repeat(self.fn,1,1);self.register_buffer(\"fk\",fk);self.ib1,self.ib2=ResidualSECNNBlockPT(imu_dim,64,3,d=d[0]),ResidualSECNNBlockPT(64,128,5,d=d[1]);self.tc1,self.tb1,self.tp1,self.td1=nn.Conv1d(t,64,3,padding=1,bias=False),nn.BatchNorm1d(64),nn.MaxPool1d(2),nn.Dropout(d[2]);self.tc2,self.tb2,self.tp2,self.td2=nn.Conv1d(64,128,3,padding=1,bias=False),nn.BatchNorm1d(128),nn.MaxPool1d(2),nn.Dropout(d[3]);self.bilstm,self.ld=nn.LSTM(256,128,bidirectional=True,batch_first=True),nn.Dropout(d[4]);self.att=type('A',(nn.Module,),{'__init__':lambda s,h:super(type(s),s).__init__()or setattr(s,'a',nn.Linear(h,1)),'forward':lambda s,x:torch.sum(x*F.softmax(torch.tanh(s.a(x)).squeeze(-1),dim=1).unsqueeze(-1),dim=1)})(256);self.d1,self.bnd1,self.dr1=nn.Linear(256,256,bias=False),nn.BatchNorm1d(256),nn.Dropout(d[5]);self.d2,self.bnd2,self.dr2=nn.Linear(256,128,bias=False),nn.BatchNorm1d(128),nn.Dropout(d[6]);self.clf=nn.Linear(128,n);self.m=torch.tensor([0,0,0,0,0,0,9e-3,1.08, -2.6e-3,3.7e-3,-5.3e-3,-2.8e-3,1.3e-3,-1.5e-4,0.63,0.62,0.60,0.62,0.63,0.65,7.4e-3,-3.4e-3,-7.5e-3,-2.6e-2,2.9e-2,-3.1e-2,-2e-3,-4.7e-3,-4.7e-3,-2.6e-2,1.5e-2,1e-2],dtype=torch.float32).view(1,-1,1).to(device);self.s=torch.tensor([1,1,1,1,1,1,0.2,0.85,0.31,0.26,0.29,0.23,0.3,0.32,1.02,0.88,0.86,1.09,1.02,0.9,0.46,0.2,0.2,1.22,0.95,0.66,0.29,0.34,0.81,0.65,1.1,1.55],dtype=torch.float32).view(1,-1,1).to(device)+1e-8\n    def forward(self,x):ir,t=x[:,:,:self.fn].transpose(1,2),x[:,:,self.fn:].transpose(1,2);ife=self.imu_fe(ir);fl=F.conv1d(ife[:,:self.fn,:],self.fk,padding=self.fk.shape[-1]//2,groups=self.fn);imu=(torch.cat([fl,ife[:,self.fn:,:]],dim=1)-self.m)/self.s;x1=self.ib2(self.ib1(imu));x2=self.td2(self.tp2(F.relu(self.tb2(self.tc2(self.td1(self.tp1(F.relu(self.tb1(self.tc1(t))))))))));lo,_=self.bilstm(torch.cat([x1,x2],dim=1).transpose(1,2));a=self.att(self.ld(lo));x=self.dr1(F.relu(self.bnd1(self.d1(a))));x=self.dr2(F.relu(self.bnd2(self.d2(x))));return self.clf(x)\ndef pad_sequences_torch(s,m,p='post',v=0.0):r=[];[r.append(i[:m]if len(i)>=m else np.concatenate([i,np.full((m-len(i),i.shape[1]),v,dtype=np.float32)]if p=='post'else[np.full((m-len(i),i.shape[1]),v,dtype=np.float32),i]))for i in s];return np.array(r,dtype=np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:08:16.807618Z","iopub.execute_input":"2025-07-09T13:08:16.808024Z","iopub.status.idle":"2025-07-09T13:08:16.836433Z","shell.execute_reply.started":"2025-07-09T13:08:16.807998Z","shell.execute_reply":"2025-07-09T13:08:16.835729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def handle_zero_norm_quaternions(q):q[np.all(q==0,axis=1)]=[0,0,0,1];return q\ndef remove_gravity_from_acc(d):q=handle_zero_norm_quaternions(d[['rot_x','rot_y','rot_z','rot_w']].values);return d[['acc_x','acc_y','acc_z']].values-R.from_quat(q).apply([0,0,9.81],inverse=True)\ndef calculate_angular_velocity_from_quat(d,dt=1/200):q=handle_zero_norm_quaternions(d[['rot_x','rot_y','rot_z','rot_w']].values);v=(R.from_quat(q[:-1]).inv()*R.from_quat(q[1:])).as_rotvec()/dt;return np.vstack([np.zeros(3),v])\ndef calculate_angular_distance(d):q=handle_zero_norm_quaternions(d[['rot_x','rot_y','rot_z','rot_w']].values);dist=np.linalg.norm((R.from_quat(q[:-1]).inv()*R.from_quat(q[1:])).as_rotvec(),axis=1);return np.insert(dist,0,0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:08:16.837988Z","iopub.execute_input":"2025-07-09T13:08:16.838240Z","iopub.status.idle":"2025-07-09T13:08:16.862883Z","shell.execute_reply.started":"2025-07-09T13:08:16.838224Z","shell.execute_reply":"2025-07-09T13:08:16.861988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SEBlockKeras(tf.keras.layers.Layer):\n    def __init__(self,r=8,**kwargs):super().__init__(**kwargs);self.r=r\n    def build(self,s):c=s[-1];self.gap=GlobalAveragePooling1D();self.d1=Dense(c//self.r,activation='relu');self.d2=Dense(c,activation='sigmoid');self.rs=Reshape((1,c));super().build(s)\n    def call(self,i):return Multiply()([i,self.rs(self.d2(self.d1(self.gap(i))))])\n    def get_config(self):c=super().get_config();c.update({'r':self.r});return c\nclass ResidualSECNNBlockKeras(tf.keras.layers.Layer):\n    def __init__(self,f,k,p=2,d=0.3,wd=1e-4,**kwargs):super().__init__(**kwargs);self.f,self.k,self.p,self.d,self.wd=f,k,p,d,wd;self.c1=Conv1D(f,k,padding='same',use_bias=False,kernel_regularizer=l2(wd));self.b1=BatchNormalization();self.c2=Conv1D(f,k,padding='same',use_bias=False,kernel_regularizer=l2(wd));self.b2=BatchNormalization();self.se=SEBlockKeras();self.pool=MaxPooling1D(p);self.drop=Dropout(d)\n    def build(self,s):self.s_conv=Conv1D(self.f,1,padding='same',use_bias=False,kernel_regularizer=l2(self.wd))if s[-1]!=self.f else None;super().build(s)\n    def call(self,i):x=Activation('relu')(self.b1(self.c1(i)));x=self.b2(self.c2(x));s=self.s_conv(i)if self.s_conv else i;x=self.se(x);x=add([x,s]);return self.drop(self.pool(Activation('relu')(x)))\n    def get_config(self):c=super().get_config();c.update({'f':self.f,'k':self.k,'p':self.p,'d':self.d,'wd':self.wd});return c\nclass TransformerEncoderKeras(tf.keras.layers.Layer):\n    def __init__(self,hs,nh,ffd,d=0.0,**kwargs):super().__init__(**kwargs);self.hs,self.nh,self.ffd,self.d=hs,nh,ffd,d;self.mha=MultiHeadAttention(key_dim=hs,num_heads=nh,dropout=d);self.d1=Dropout(d);self.ln1=LayerNormalization(epsilon=1e-6);self.d_ff1=Dense(ffd,activation=\"relu\");self.d_ff2=Dense(ffd);self.d2=Dropout(d);self.ln2=LayerNormalization(epsilon=1e-6)\n    def build(self,s):self.out_d=Dense(s[-1]);super().build(s)\n    def call(self,i):x=self.ln1(i);x=self.mha(x,x);x=self.d1(x);res=x+i;x=self.ln2(res);x=self.d_ff1(x);x=self.d_ff2(x);x=self.d2(x);x=self.out_d(x);return x+res\n    def get_config(self):c=super().get_config();c.update({'hs':self.hs,'nh':self.nh,'ffd':self.ffd,'d':self.d});return c\ndef build_transformer_model(pad_len,imu_dim,tof_dim,n_classes,wd=1e-4):\n    inp=Input(shape=(pad_len,imu_dim+tof_dim));x=GaussianNoise(0.01)(inp);imu,tof=Lambda(lambda t:t[:,:,:imu_dim])(x),Lambda(lambda t:t[:,:,imu_dim:])(x)\n    x1=ResidualSECNNBlockKeras(64,3,d=0.2,wd=wd)(imu);x1=ResidualSECNNBlockKeras(128,5,d=0.2,wd=wd)(x1)\n    x2_base=ResidualSECNNBlockKeras(64,3,d=0.25,wd=wd)(tof);x2_base=ResidualSECNNBlockKeras(128,3,d=0.25,wd=wd)(x2_base)\n    gate=Dense(1,'sigmoid',name='tof_gate')(GlobalAveragePooling1D()(tof));x2=Multiply()([x2_base,gate])\n    x=Concatenate()([x1,x2]);x=Bidirectional(LSTM(256,return_sequences=True,kernel_regularizer=l2(wd)))(x)\n    x=TransformerEncoderKeras(128,4,x.shape[-1],d=0.3)(x);x=GlobalAveragePooling1D()(x)\n    for u,d in[(512,0.5),(128,0.3)]:x=Dropout(d)(Activation('relu')(BatchNormalization()(Dense(u,use_bias=False,kernel_regularizer=l2(wd))(x))))\n    return Model(inputs=inp,outputs=[Dense(n_classes,'softmax',name='main_output')(x),gate])\nclass GatedMixupGenerator(Sequence):\n    def __init__(self,X,y,b,id,w=None,a=0.2,m=0.0):self.X,self.y,self.b,self.id,self.w,self.a,self.m=X,y,b,id,w,a,m;self.ix=np.arange(len(X))\n    def __len__(self):return int(np.ceil(len(self.X)/self.b))\n    def __getitem__(self,i):ix=self.ix[i*self.b:(i+1)*self.b];Xb,yb=self.X[ix].copy(),self.y[ix].copy();sw=np.array([self.w.get(i,1.0)for i in yb.argmax(1)])if self.w else np.ones(len(Xb));gt=np.ones(len(Xb));[self._mask(Xb,j,gt)for j in range(len(Xb))if np.random.rand()<self.m];l,p=np.random.beta(self.a,self.a),np.random.permutation(len(Xb));Xm,ym=l*Xb+(1-l)*Xb[p],l*yb+(1-l)*yb[p];gtm,swm=l*gt+(1-l)*gt[p],l*sw+(1-l)*sw[p];return(Xm,{'main_output':ym,'tof_gate':gtm},swm)if self.a>0 else(Xb,{'main_output':yb,'tof_gate':gt},sw)\n    def _mask(self,Xb,j,gt):Xb[j,:,self.id:]=0;gt[j]=0.0\n    def on_epoch_end(self):np.random.shuffle(self.ix)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-09T13:08:16.865223Z","iopub.execute_input":"2025-07-09T13:08:16.865456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    print(\"▶ 学習モード開始\")\n    # (このセクションの大部分は変更ありません)\n    df = pd.read_csv(RAW_DIR/\"train.csv\").merge(pd.read_csv(RAW_DIR/\"train_demographics.csv\"), on='subject')\n    le = LabelEncoder()\n    df['gesture_int'] = le.fit_transform(df['gesture'])\n    \n    print(\"  特徴量生成中...\")\n    all_sequences = []\n    # (特徴量生成のループ... 変更なし)\n    for _, seq_df in tqdm(df.groupby('sequence_id'), desc=\"特徴量生成\"):\n        s = seq_df.copy().fillna(method='ffill').fillna(method='bfill').fillna(0)\n        s[['linear_acc_x','linear_acc_y','linear_acc_z']] = remove_gravity_from_acc(s)\n        s[['angular_vel_x','angular_vel_y','angular_vel_z']] = calculate_angular_velocity_from_quat(s)\n        s['linear_acc_mag'] = np.linalg.norm(s[['linear_acc_x','linear_acc_y','linear_acc_z']].values, axis=1)\n        s['linear_acc_mag_jerk'] = s['linear_acc_mag'].diff().fillna(0)\n        s['angular_distance'] = calculate_angular_distance(s)\n        for i in range(1, 6):\n            if f'tof_{i}_v0' in s.columns:\n                pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]\n                tof_data = s[pixel_cols].replace(-1, np.nan)\n                s[f\"tof_{i}_mean\"], s[f\"tof_{i}_std\"] = tof_data.mean(axis=1), tof_data.std(axis=1)\n        all_sequences.append(s)\n    \n    df = pd.concat(all_sequences).fillna(0)\n    \n    imu_cols = [c for c in df.columns if any(s in c for s in ['acc_','rot_','angular_']) and 'uncalib' not in c]\n    tof_cols = [c for c in df.columns if 'tof_' in c or 'thm_' in c]\n    final_feature_cols = imu_cols + tof_cols\n    imu_dim, tof_dim = len(imu_cols), len(tof_cols)\n    print(f\"  特徴量数: IMU={imu_dim}, TOF/THM={tof_dim}, Total={len(final_feature_cols)}\")\n    \n    X_list, y_list, groups, lens = [], [], [], []\n    for _, seq_df in df.groupby('sequence_id'):\n        X_list.append(seq_df[final_feature_cols].values.astype('float32'))\n        y_list.append(seq_df['gesture_int'].iloc[0])\n        groups.append(seq_df['subject'].iloc[0])\n        lens.append(len(seq_df))\n    \n    scaler = StandardScaler().fit(np.concatenate(X_list, axis=0))\n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    X = pad_sequences([scaler.transform(x) for x in X_list], maxlen=pad_len, padding='post', dtype='float32')\n    y_cat, y_stratify = to_categorical(y_list), np.array(y_list)\n    \n    oof_A = np.zeros_like(y_cat)\n    sgkf = StratifiedGroupKFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n    for fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y_stratify, groups)):\n        print(f\"\\n===== FOLD {fold+1}/{N_SPLITS} =====\")\n        X_tr, X_val, y_tr, y_val = X[train_idx], X[val_idx], y_cat[train_idx], y_cat[val_idx]\n        model = build_transformer_model(pad_len, imu_dim, tof_dim, len(le.classes_), wd=WD)\n        model.compile(optimizer=AdamTF(CosineDecay(LR_INIT, len(X_tr)//BATCH_SIZE*EPOCHS)),\n                      loss={'main_output': 'categorical_crossentropy', 'tof_gate': 'binary_crossentropy'},\n                      loss_weights={'main_output': 1.0, 'tof_gate': GATE_LOSS_WEIGHT},\n                      metrics={'main_output': 'accuracy'})\n        cw = dict(enumerate(compute_class_weight('balanced', classes=np.unique(y_list), y=y_tr.argmax(1))))\n        train_gen = GatedMixupGenerator(X_tr, y_tr, BATCH_SIZE, imu_dim, w=cw, a=MIXUP_ALPHA, m=MASKING_PROB)\n        val_gen = GatedMixupGenerator(X_val, y_val, BATCH_SIZE, imu_dim)\n        cb = EarlyStopping(patience=PATIENCE, restore_best_weights=True, verbose=1, monitor='val_main_output_accuracy', mode='max')\n        model.fit(train_gen, epochs=EPOCHS, validation_data=val_gen, callbacks=[cb], verbose=1)\n        \n        # ▼▼▼【ここが修正点】▼▼▼\n        # model.save() の代わりに model.export() を使用する\n        model.export(EXPORT_DIR / f\"model_A_fold_{fold}\")\n        \n        oof_A[val_idx], _ = model.predict(X_val, batch_size=BATCH_SIZE)\n\n    if CompetitionMetric is not None:\n        oof_score = CompetitionMetric().calculate_hierarchical_f1(pd.DataFrame({'gesture':le.classes_[y_cat.argmax(1)]}), pd.DataFrame({'gesture':le.classes_[oof_A.argmax(1)]}))\n        print(f\"\\n\\n=================================================\\n  CVスコア (OOF H-F1 Score): {oof_score:.4f}\\n=================================================\\n\")\n    \n    joblib.dump(scaler, EXPORT_DIR/\"scaler.pkl\")\n    np.save(EXPORT_DIR/\"final_feature_cols.npy\", np.array(final_feature_cols))\n    np.save(EXPORT_DIR/\"gesture_classes.npy\", le.classes_)\n    np.save(EXPORT_DIR/\"sequence_maxlen.npy\", pad_len)\n    print(\"✔ 学習と成果物の保存が完了しました。\")\n\nelse:\n    print(\"▶ 推論モード開始\")\n    # --- アーティファクトのロード ---\n    final_feature_cols_A=np.load(YOUR_MODELS_DIR/\"final_feature_cols.npy\",allow_pickle=True).tolist()\n    scaler_A=joblib.load(YOUR_MODELS_DIR/\"scaler.pkl\")\n    gesture_classes=np.load(YOUR_MODELS_DIR/\"gesture_classes.npy\",allow_pickle=True)\n    pad_len_A=int(np.load(YOUR_MODELS_DIR/\"sequence_maxlen.npy\"))\n    \n    # --- モデルのロード ---\n    \n    # ▼▼▼【ここが修正点】▼▼▼\n    # Keras 3の仕様変更に対応し、TFSMLayerを使ってモデルをロードする\n    print(\"  自作モデル（モデルA）をロード中...\")\n    models_A = []\n    for f in range(N_SPLITS):\n        model_path = str(YOUR_MODELS_DIR / f\"model_A_fold_{f}\")\n        # TFSMLayerをラップする新しい推論専用モデルを構築\n        input_shape = (pad_len_A, len(final_feature_cols_A))\n        inp = Input(shape=input_shape, name=\"serving_default_inp\") # SavedModelの入力名に合わせる\n        tfsmlayer = TFSMLayer(model_path, call_endpoint='serving_default')\n        outputs = tfsmlayer(inp)\n        # Kerasの仕様上、出力は辞書形式になるため、必要なキーを指定して抽出する\n        main_output = outputs['main_output']\n        gate_output = outputs['tof_gate']\n        inference_model = Model(inp, [main_output, gate_output])\n        models_A.append(inference_model)\n\n    print(\"  公開モデル（モデルB）をロード中...\")\n    # モデルBは .h5 形式なので、従来の方法でロード可能\n    def time_sum_fn(x):return K.sum(x,1)\n    def squeeze_fn(x):return tf.squeeze(x,-1)\n    def expand_fn(x):return tf.expand_dims(x,-1)\n    def se_block_fn(x,r=8):c=x.shape[-1];s=Dense(c//r,'relu')(GlobalAveragePooling1D()(x));s=Reshape((1,c))(Dense(c,'sigmoid')(s));return Multiply()([x,s])\n    def res_se_cnn_fn(x,f,k,p=2,d=0.3,wd=1e-4):s=x;x=Conv1D(f,k,padding='same',use_bias=False,kernel_regularizer=l2(wd))(x);x=BatchNormalization()(x);x=Activation('relu')(x);x=Conv1D(f,k,padding='same',use_bias=False,kernel_regularizer=l2(wd))(x);x=BatchNormalization()(x);x=se_block_fn(x);s=Conv1D(f,1,use_bias=False,kernel_regularizer=l2(wd))(s) if s.shape[-1]!=f else s;x=add([x,s]);x=Activation('relu')(x);x=MaxPooling1D(p)(x);x=Dropout(d)(x);return x\n    def attention_fn(x):w=Activation('softmax')(Lambda(squeeze_fn)(Dense(1,'tanh')(x)));return Lambda(time_sum_fn)(Multiply()([x,Lambda(expand_fn)(w)]))\n    custom_objs_B={'time_sum':time_sum_fn,'squeeze_last_axis':squeeze_fn,'expand_last_axis':expand_fn,'se_block':se_block_fn,'residual_se_cnn_block':res_se_cnn_fn,'attention_layer':attention_fn}\n    model_B=load_model(PUBLIC_TF_MODEL_DIR/\"gesture_two_branch_mixup.h5\",custom_objects=custom_objs_B,compile=False)\n\n    print(\"  公開モデル（モデルC）をロード中...\")\n    pt_models=[]\n    for f in range(N_SPLITS):\n        try:ckpt=torch.load(PUBLIC_PT_MODEL_DIR/f\"gesture_two_branch_fold{f}.pth\",map_location=device);m=PublicTwoBranchModel(ckpt['pad_len'],ckpt['imu_dim'],ckpt['tof_dim'],ckpt['n_classes']).to(device);m.load_state_dict(ckpt['model_state_dict']);m.eval();pt_models.append(m)\n        except:pass\n    \n    print(f\"✔ モデルのロード完了\")\n\n\n    def predict_final(sequence:pl.DataFrame,demographics:pl.DataFrame)->str:\n        import numpy as np_local;from tensorflow.keras.utils import pad_sequences as pad_sequences_local;from scipy.stats import rankdata as rankdata_local;import torch as torch_local\n        scaler_B=joblib.load(PUBLIC_TF_MODEL_DIR/\"scaler.pkl\");final_feature_cols_B=np_local.load(PUBLIC_TF_MODEL_DIR/\"feature_cols.npy\",allow_pickle=True).tolist();pad_len_B=int(np_local.load(PUBLIC_TF_MODEL_DIR/\"sequence_maxlen.npy\"))\n        scaler_C=joblib.load(PUBLIC_PT_MODEL_DIR/\"scaler.pkl\");final_feature_cols_C=np_local.load(PUBLIC_PT_MODEL_DIR/\"feature_cols.npy\",allow_pickle=True).tolist();pad_len_C=int(np_local.load(PUBLIC_PT_MODEL_DIR/\"sequence_maxlen.npy\"))\n        df=sequence.to_pandas().fillna(method='ffill').fillna(method='bfill').fillna(0)\n        df_A=df.copy();df_A[['linear_acc_x','linear_acc_y','linear_acc_z']]=remove_gravity_from_acc(df_A);df_A[['angular_vel_x','angular_vel_y','angular_vel_z']]=calculate_angular_velocity_from_quat(df_A);df_A['linear_acc_mag']=np_local.linalg.norm(df_A[['linear_acc_x','linear_acc_y','linear_acc_z']].values,axis=1);df_A['linear_acc_mag_jerk']=df_A['linear_acc_mag'].diff().fillna(0);df_A['angular_distance']=calculate_angular_distance(df_A)\n        for i in range(1,6):\n            if f'tof_{i}_v0' in df_A.columns:p=[f\"tof_{i}_v{p}\"for p in range(64)];t=df_A[p].replace(-1,np_local.nan);df_A[f\"tof_{i}_mean\"],df_A[f\"tof_{i}_std\"]=t.mean(axis=1),t.std(axis=1)\n        df_A=df_A.fillna(0);mat_A=scaler_A.transform(df_A[final_feature_cols_A].values.astype(np_local.float32));pad_A=pad_sequences_local([mat_A],maxlen=pad_len_A,padding='post',dtype='float32');pred_A=np_local.mean([m.predict(pad_A,verbose=0)[0]for m in models_A],axis=0)\n        df_B=df.copy();df_B['acc_mag']=np_local.sqrt(df_B['acc_x']**2+df_B['acc_y']**2+df_B['acc_z']**2);df_B['rot_angle']=2*np_local.arccos(df_B['rot_w'].clip(-1,1));df_B['acc_mag_jerk']=df_B['acc_mag'].diff().fillna(0);df_B['rot_angle_vel']=df_B['rot_angle'].diff().fillna(0);df_B[['linear_acc_x','linear_acc_y','linear_acc_z']]=remove_gravity_from_acc(df_B);df_B['linear_acc_mag']=np_local.linalg.norm(df_B[['linear_acc_x','linear_acc_y','linear_acc_z']].values,axis=1);df_B['linear_acc_mag_jerk']=df_B['linear_acc_mag'].diff().fillna(0);df_B[['angular_vel_x','angular_vel_y','angular_vel_z']]=calculate_angular_velocity_from_quat(df_B);df_B['angular_distance']=calculate_angular_distance(df_B);df_B=df_B.fillna(0)\n        mat_B=scaler_B.transform(df_B[final_feature_cols_B].values.astype(np_local.float32));pad_B=pad_sequences_local([mat_B],maxlen=pad_len_B,padding='post',dtype='float32');pred_B=model_B.predict(pad_B,verbose=0)[0]\n        df_C=df.copy().fillna(0);mat_C=scaler_C.transform(df_C[final_feature_cols_C].values.astype(np_local.float32));pad_C=pad_sequences_torch([mat_C],maxlen=pad_len_C,padding='post')\n        with torch.no_grad():pt_input=torch.from_numpy(pad_C).to(device);preds_C=[m(pt_input)for m in pt_models];pred_C=torch.mean(torch.stack(preds_C),dim=0).cpu().numpy()[0]\n        final_rank=(rankdata_local(pred_A)+rankdata_local(pred_B)+rankdata_local(pred_C))/3.0\n        return str(gesture_classes[np_local.argmax(final_rank)])\n\n    if not os.getenv('KAGGLE_IS_COMPETITION_RERUN')and CompetitionMetric is not None:\n        print(\"\\n▶ 推論パイプラインでCVスコアを再計算します...\")\n        val_df=pd.read_csv(RAW_DIR/\"train.csv\");all_preds,all_trues=[],[]\n        for _,seq_df in tqdm(val_df.groupby('sequence_id'),desc=\"CVスコア計算中\"):true_gesture=seq_df['gesture'].iloc[0];pred_gesture=predict_final(pl.from_pandas(seq_df),None);all_preds.append({'gesture':pred_gesture});all_trues.append({'gesture':true_gesture})\n        cv_score_recalc=CompetitionMetric().calculate_hierarchical_f1(pd.DataFrame(all_trues),pd.DataFrame(all_preds));print(f\"\\n=================================================\\n  CVスコア (再計算値): {cv_score_recalc:.4f}\\n=================================================\\n\")\n    \n    print(\"▶ Kaggle推論サーバーをセットアップします...\")\n    if'kaggle_evaluation'not in locals():import kaggle_evaluation.cmi_inference_server\n    inference_server=kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict_final)\n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):inference_server.serve()\n    else:inference_server.run_local_gateway(data_paths=(f'{RAW_DIR}/test.csv',f'{RAW_DIR}/test_demographics.csv'))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}