{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":31254,"databundleVersionId":3103714,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 1. Import and helper functions","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom tqdm.notebook import tqdm\nimport gc\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.layers import (Input, Embedding, Dropout, SpatialDropout1D, \n                                     BatchNormalization, LSTM, Dense, Concatenate,                                                                        \n                                     MultiHeadAttention, LayerNormalization, Add, \n                                     GlobalAveragePooling1D)\nfrom tensorflow.keras.models import Model, Sequential, load_model\nfrom tensorflow.keras.utils import pad_sequences\nfrom tensorflow.keras import regularizers\n\nnp.random.seed(42)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:29.281631Z","iopub.execute_input":"2026-01-02T10:31:29.282331Z","iopub.status.idle":"2026-01-02T10:31:35.035002Z","shell.execute_reply.started":"2026-01-02T10:31:29.282293Z","shell.execute_reply":"2026-01-02T10:31:35.034379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def mapK(pred_items, target_item, K=12):\n    \"\"\"\n    pred_items : list of K recommended item_id (order from 1 to K)\n    target_item : correct item_id\n    \"\"\"\n    score = 0.0\n    for k in range(1, K + 1):\n        if pred_items[k-1] == target_item:\n            score = 1.0 / k\n            break\n    return score\n\n\ndef map12_score(preds, targets):\n    total_score = 0\n    for p, t in zip(preds, targets):\n        if t in p:\n            # Знаходимо позицію (1-based index)\n            rank = np.where(p == t)[0][0] + 1\n            total_score += 1 / rank\n    return total_score / len(targets)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:35.036263Z","iopub.execute_input":"2026-01-02T10:31:35.036774Z","iopub.status.idle":"2026-01-02T10:31:35.042415Z","shell.execute_reply.started":"2026-01-02T10:31:35.036746Z","shell.execute_reply":"2026-01-02T10:31:35.041614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def model_predict_last_item(history):\n    return history[-1]\n\ndef model_predict_last_12_item(history):\n    return history[-12:][::-1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:35.043282Z","iopub.execute_input":"2026-01-02T10:31:35.043619Z","iopub.status.idle":"2026-01-02T10:31:35.073931Z","shell.execute_reply.started":"2026-01-02T10:31:35.043596Z","shell.execute_reply":"2026-01-02T10:31:35.073301Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_top_k_predictions(model, dataset, k=12):\n    all_predicted_indices = []\n    all_actual_indices = []\n\n    for x_batch, y_batch in dataset:\n        # 1. Get probability distribution from model\n        preds = model.predict(x_batch, verbose=0)\n        \n        # 2. Extract indices of the top K probabilities\n        # values: the probabilities, indices: the item integer IDs\n        values, indices = tf.math.top_k(preds, k=k)\n        \n        all_predicted_indices.extend(indices.numpy().tolist())\n        all_actual_indices.extend(y_batch.numpy().tolist())\n        \n    return all_actual_indices, all_predicted_indices","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:35.075363Z","iopub.execute_input":"2026-01-02T10:31:35.075627Z","iopub.status.idle":"2026-01-02T10:31:35.087977Z","shell.execute_reply.started":"2026-01-02T10:31:35.075607Z","shell.execute_reply":"2026-01-02T10:31:35.087409Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_history(history):\n    plt.figure(figsize=(12, 5))\n    \n    plt.subplot(1, 3, 1)\n    plt.plot(history.history['loss'], label='Train Loss', marker='o')\n    plt.plot(history.history['val_loss'], label='Val Loss', marker='o')\n    plt.title('Model Loss')\n    plt.xlabel('Epoch')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.grid(True)\n    \n    plt.subplot(1, 3, 2)\n    plt.plot(history.history['accuracy'], label='Train Acc', marker='o')\n    plt.plot(history.history['val_accuracy'], label='Val Acc', marker='o')\n    plt.title('Model Accuracy')\n    plt.xlabel('Epoch')\n    plt.ylabel('Accuracy')\n    plt.legend()\n    plt.grid(True)\n    \n    plt.subplot(1, 3, 3)\n    plt.plot(history.history['top_12_accuracy'], label='Train Top 12 Acc', marker='o')\n    plt.plot(history.history['val_top_12_accuracy'], label='Val Top 12 Acc', marker='o')\n    plt.title('Model Top 12 Accuracy')\n    plt.xlabel('Epoch')\n    plt.ylabel('Accuracy')\n    plt.legend()\n    plt.grid(True)\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:35.088794Z","iopub.execute_input":"2026-01-02T10:31:35.089087Z","iopub.status.idle":"2026-01-02T10:31:35.105473Z","shell.execute_reply.started":"2026-01-02T10:31:35.089024Z","shell.execute_reply":"2026-01-02T10:31:35.104771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_predict(model, test_loader, model_type='single_input'):\n    all_top_12 = []\n    \n    # Iterate through batches with DataLoader\n    for x_batch, _ in tqdm(test_loader, desc=\"Predicting batches\"):\n        # Probabilities for current batch\n        if model_type == 'multi_input':\n            batch_preds = model.predict_on_batch(x_batch)\n        else: \n            batch_preds = model.predict_on_batch(x_batch['input_art'])\n        \n        # Top-12 indexes (greatest probabilities)\n        # np.argsort - indexes from smallest to biggest\n        # [:, -12:] takes 12 best, [:, ::-1] reverts them in correct order\n        batch_top_12 = np.argsort(batch_preds, axis=1)[:, -12:][:, ::-1]\n        del batch_preds\n        \n        # Save only these 12 numbers\n        all_top_12.append(batch_top_12)\n        del batch_top_12\n        \n        gc.collect()\n    \n    # 3. Join all batches\n    top_12_indices = np.vstack(all_top_12)\n    del all_top_12  \n    \n    print(f\"\\nReady! Calculated predictions for {top_12_indices.shape[0]} users.\")\n    print(f\"Output format: {top_12_indices.shape}\") \n    \n    return top_12_indices","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:35.106342Z","iopub.execute_input":"2026-01-02T10:31:35.106574Z","iopub.status.idle":"2026-01-02T10:31:35.121704Z","shell.execute_reply.started":"2026-01-02T10:31:35.106542Z","shell.execute_reply":"2026-01-02T10:31:35.120668Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load data","metadata":{}},{"cell_type":"code","source":"transactions_train = pd.read_csv('/kaggle/input/h-and-m-personalized-fashion-recommendations/transactions_train.csv',\n                                usecols=['t_dat', 'customer_id', 'article_id'])\n\nunique_transactions = transactions_train.drop_duplicates(subset=['t_dat', 'customer_id', 'article_id'])\ndel transactions_train\npurchase_counts = unique_transactions.groupby(\"customer_id\")[\"article_id\"].count()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:31:35.123423Z","iopub.execute_input":"2026-01-02T10:31:35.123812Z","iopub.status.idle":"2026-01-02T10:33:00.971906Z","shell.execute_reply.started":"2026-01-02T10:31:35.123783Z","shell.execute_reply":"2026-01-02T10:33:00.971303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"articles_df = pd.read_csv('/kaggle/input/h-and-m-personalized-fashion-recommendations/articles.csv',\n                         usecols=['article_id', 'product_group_name', 'product_type_name', 'colour_group_code'])\narticles_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:00.972794Z","iopub.execute_input":"2026-01-02T10:33:00.973028Z","iopub.status.idle":"2026-01-02T10:33:01.607795Z","shell.execute_reply.started":"2026-01-02T10:33:00.973005Z","shell.execute_reply":"2026-01-02T10:33:01.607029Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Data preprocessing","metadata":{}},{"cell_type":"code","source":"N_USERS_TRAIN_TEST = 50000 # Number of top users that we select to speed up comparison of models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:01.608829Z","iopub.execute_input":"2026-01-02T10:33:01.609386Z","iopub.status.idle":"2026-01-02T10:33:01.612665Z","shell.execute_reply.started":"2026-01-02T10:33:01.609353Z","shell.execute_reply":"2026-01-02T10:33:01.611950Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"purchase_counts.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:01.614791Z","iopub.execute_input":"2026-01-02T10:33:01.615055Z","iopub.status.idle":"2026-01-02T10:33:01.629887Z","shell.execute_reply.started":"2026-01-02T10:33:01.615035Z","shell.execute_reply":"2026-01-02T10:33:01.629387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"top_users = purchase_counts.sort_values(ascending=False).head(N_USERS_TRAIN_TEST).index.tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:01.630747Z","iopub.execute_input":"2026-01-02T10:33:01.631032Z","iopub.status.idle":"2026-01-02T10:33:01.886224Z","shell.execute_reply.started":"2026-01-02T10:33:01.630999Z","shell.execute_reply":"2026-01-02T10:33:01.885583Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_top = unique_transactions[unique_transactions.customer_id.isin(top_users)].reset_index(drop=True)\ndf_top = df_top.merge(articles_df, how='left', on=['article_id'], validate='m:1', )\n\ndef encode_column(df, column_name):\n    unique_values = df[column_name].unique()\n    \n    val2idx = {val: i + 1 for i, val in enumerate(unique_values)}\n    idx2val = {i + 1: val for i, val in enumerate(unique_values)}\n    \n    encoded_series = df[column_name].map(val2idx)\n    \n    vocab_size = len(val2idx) + 1\n    \n    return encoded_series, val2idx, idx2val, vocab_size\n\ndf_top['article'], art2idx, idx2art, article_vocab_size = encode_column(df_top, 'article_id')\ndf_top['cust'], cust2idx, idx2cust, customer_vocab_size = encode_column(df_top, 'customer_id')\ndf_top['product_type'], prod_type2idx, idx2prod_type, prod_type_vocab_size = encode_column(df_top, 'product_type_name')\ndf_top['product_group'], prod_group2idx, idx2prod_group, prod_group_vocab_size = encode_column(df_top, 'product_group_name')\ndf_top['colour_group'], color2idx, idx2color, color_vocab_size = encode_column(df_top, 'colour_group_code')\n\ndf_top = df_top.drop(columns=['customer_id', 'article_id', 'product_type_name', 'product_group_name'], errors='ignore')\ndf_top","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:01.887082Z","iopub.execute_input":"2026-01-02T10:33:01.887334Z","iopub.status.idle":"2026-01-02T10:33:12.282552Z","shell.execute_reply.started":"2026-01-02T10:33:01.887307Z","shell.execute_reply":"2026-01-02T10:33:12.281759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_top.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:12.283522Z","iopub.execute_input":"2026-01-02T10:33:12.283816Z","iopub.status.idle":"2026-01-02T10:33:12.288249Z","shell.execute_reply.started":"2026-01-02T10:33:12.283785Z","shell.execute_reply":"2026-01-02T10:33:12.287527Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Train / test split","metadata":{}},{"cell_type":"code","source":"top_users = np.array(top_users)\ntop_users_le = pd.Series(top_users).map(cust2idx).values\ntop_users_le[:2]\n\nnp.random.shuffle(top_users_le)\ntrain_size = int(0.99 * len(top_users_le))\n\ntrain_users = set(top_users_le[:train_size])\ntest_users  = set(top_users_le[train_size:])\nprint('Users train / test:', len(train_users), len(test_users))\n\ntrain_df = df_top[df_top['cust'].isin(train_users)].copy()\ntest_df  = df_top[df_top['cust'].isin(test_users)].copy()\n\nprint('Samples train / test:', train_df.shape, test_df.shape)\n\ntrain_df = train_df.sort_values(['cust', 't_dat'])\ntest_df  = test_df.sort_values(['cust', 't_dat'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:12.289093Z","iopub.execute_input":"2026-01-02T10:33:12.289294Z","iopub.status.idle":"2026-01-02T10:33:15.186239Z","shell.execute_reply.started":"2026-01-02T10:33:12.289275Z","shell.execute_reply":"2026-01-02T10:33:15.185625Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MAX_LEN = 50\ndef prepare_multi_input_data(df, max_len, is_train=True, max_examples_per_user=30):\n    # Групуємо дані по юзерах один раз\n    # Порядок у списках відповідатиме t_dat, якщо df відсортовано\n    grouped = df.sort_values(['cust', 't_dat']).groupby('cust')\n    \n    X_art, X_type, X_group, X_col = [], [], [], []\n    y = []\n\n    for cust_id, group in tqdm(grouped, desc=\"Processing Users\"):\n        # Перетворюємо колонки групи в масиви для швидкості\n        articles = group['article'].values\n        types = group['product_type'].values\n        groups = group['product_group'].values\n        colors = group['colour_group'].values\n        \n        if len(articles) < 2:\n            continue\n            \n        if is_train:\n            # Логіка \"свіжості\": беремо лише хвіст історії для створення прикладів\n            # Нам потрібно (max_examples + 1) елементів, щоб отримати max_examples пар (X, y)\n            start_pos = max(1, len(articles) - max_examples_per_user)\n            \n            for i in range(start_pos, len(articles)):\n                # Історія — все до поточного індексу i\n                X_art.append(articles[:i][-max_len:])\n                X_type.append(types[:i][-max_len:])\n                X_group.append(groups[:i][-max_len:])\n                X_col.append(colors[:i][-max_len:])\n                # Ціль — елемент на індексі i\n                y.append(articles[i])\n        else:\n            # Для тесту/валідації — тільки один (найсвіжіший) приклад на юзера\n            X_art.append(articles[:-1][-max_len:])\n            X_type.append(types[:-1][-max_len:])\n            X_group.append(groups[:-1][-max_len:])\n            X_col.append(colors[:-1][-max_len:])\n            y.append(articles[-1])\n\n    # Спільні параметри паддінгу\n    pad_cfg = {'maxlen': max_len, 'padding': 'pre', 'dtype': 'int32', 'value': 0}\n    \n    X_dict = {\n        'input_art': pad_sequences(X_art, **pad_cfg),\n        'input_type': pad_sequences(X_type, **pad_cfg),\n        'input_group': pad_sequences(X_group, **pad_cfg),\n        'input_col': pad_sequences(X_col, **pad_cfg)\n    }\n    \n    return X_dict, np.array(y, dtype='int32')\n\n# Виклик прямо з DataFrame\nX_train, y_train = prepare_multi_input_data(train_df, MAX_LEN, is_train=True)\nX_test, y_test = prepare_multi_input_data(test_df, MAX_LEN, is_train=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:15.187181Z","iopub.execute_input":"2026-01-02T10:33:15.187471Z","iopub.status.idle":"2026-01-02T10:33:36.489836Z","shell.execute_reply.started":"2026-01-02T10:33:15.187440Z","shell.execute_reply":"2026-01-02T10:33:36.489148Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 1024*4\n\ndef get_dataloader(X, y, batch_size=1024, shuffle=True):\n    ds = tf.data.Dataset.from_tensor_slices((X, y))\n    if shuffle:\n        ds = ds.shuffle(100000)\n    return ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\ntrain_loader = get_dataloader(X_train, y_train, batch_size=BATCH_SIZE)\ntest_loader = get_dataloader(X_test, y_test, batch_size=BATCH_SIZE, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:36.490725Z","iopub.execute_input":"2026-01-02T10:33:36.490959Z","iopub.status.idle":"2026-01-02T10:33:38.598845Z","shell.execute_reply.started":"2026-01-02T10:33:36.490936Z","shell.execute_reply":"2026-01-02T10:33:38.598268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"del articles_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:38.599704Z","iopub.execute_input":"2026-01-02T10:33:38.599940Z","iopub.status.idle":"2026-01-02T10:33:38.603599Z","shell.execute_reply.started":"2026-01-02T10:33:38.599917Z","shell.execute_reply":"2026-01-02T10:33:38.602879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_top = unique_transactions[unique_transactions.customer_id.isin(top_users)].reset_index(drop=True)\ndel unique_transactions\n\ndef encode_column(df, column_name):\n    unique_values = df[column_name].unique()\n    \n    val2idx = {val: i + 1 for i, val in enumerate(unique_values)}\n    idx2val = {i + 1: val for i, val in enumerate(unique_values)}\n    \n    encoded_series = df[column_name].map(val2idx)\n    \n    vocab_size = len(val2idx) + 1\n    \n    return encoded_series, val2idx, idx2val, vocab_size\n\ndf_top['article'], art2idx, idx2art, article_vocab_size = encode_column(df_top, 'article_id')\ndf_top['cust'], cust2idx, idx2cust, customer_vocab_size = encode_column(df_top, 'customer_id')\n\ndf_top = df_top.drop(columns=['customer_id', 'article_id'])\ndf_top","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:38.604429Z","iopub.execute_input":"2026-01-02T10:33:38.604890Z","iopub.status.idle":"2026-01-02T10:33:45.406191Z","shell.execute_reply.started":"2026-01-02T10:33:38.604866Z","shell.execute_reply":"2026-01-02T10:33:45.405405Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Baseline predict","metadata":{}},{"cell_type":"code","source":"map_scores = []\nnum_samples = len(X_test['input_art'])\n\nfor i in tqdm(range(num_samples), desc=\"Calculating MAP@12\"):\n    # Отримуємо історію покупок (тільки артикули) для Baseline\n    # Ми беремо i-й рядок з масиву 'input_art'\n    history_art = X_test['input_art'][i]\n    target = y_test[i]\n    \n    # Baseline функція: вона зазвичай дивиться на історію артикулів\n    # і вибирає останні 12 унікальних значень\n    pred = model_predict_last_12_item(history_art)\n    \n    # Рахуємо MAP@12 для цього юзера\n    map_scores.append(mapK(pred, target))\n\nbaseline_score = np.mean(map_scores)\nprint(f\"\\nBaseline MAP@12 (Last 12 items): {baseline_score:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:33:45.407201Z","iopub.execute_input":"2026-01-02T10:33:45.407599Z","iopub.status.idle":"2026-01-02T10:33:45.465588Z","shell.execute_reply.started":"2026-01-02T10:33:45.407575Z","shell.execute_reply":"2026-01-02T10:33:45.464839Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. LSTM with features","metadata":{}},{"cell_type":"code","source":"def build_multi_input_model():\n    # 1. Визначаємо входи (назви мають збігатися з ключами у нашому X_dict)\n    input_art = Input(shape=(MAX_LEN,), name='input_art')\n    input_type = Input(shape=(MAX_LEN,), name='input_type')\n    input_group = Input(shape=(MAX_LEN,), name='input_group')\n    input_col = Input(shape=(MAX_LEN,), name='input_col')\n    \n    emb_art = Embedding(article_vocab_size, 512, name='emb_art')(input_art)\n    emb_type = Embedding(prod_type_vocab_size, 64, name='emb_type')(input_type)\n    emb_group = Embedding(prod_group_vocab_size, 64, name='emb_group')(input_group)\n    emb_col = Embedding(color_vocab_size, 64, name='emb_col')(input_col)\n    \n    merged = Concatenate()([emb_art, emb_type, emb_group, emb_col])    \n    x = SpatialDropout1D(0.4)(merged) \n    \n    lstm_out = LSTM(128)(x)  \n    # lstm_out = LSTM(128, return_sequences=True)(x)  # For using attention layer\n    x = BatchNormalization()(lstm_out)\n    \n    # Multi-Head Attention Layer\n    # This allows the model to focus on specific important past articles\n    #attention_out = MultiHeadAttention(num_heads=4, key_dim=64)(lstm_out, lstm_out)\n    \n    # Residual Connection & Layer Normalization (Transformer-style)\n    #x = Add()([lstm_out, attention_out]) \n    #x = LayerNormalization()(x)\n    \n    # Pooling to condense the 50 time-steps into one feature vector\n    #x = GlobalAveragePooling1D()(x)\n    \n    x = Dense(512, activation='relu', kernel_regularizer=regularizers.l2(1e-4))(x)\n    x = BatchNormalization()(x)\n    x = Dropout(0.5)(x)    \n    \n    x = Dense(512, activation='relu')(x)\n    x = BatchNormalization()(x)\n    x = Dropout(0.5)(x)\n    \n    # 6. Вихідний шар (передбачаємо тільки ID наступного артикула)\n    output = Dense(article_vocab_size, activation='softmax', name='output')(x)\n    \n    model = Model(\n        inputs=[input_art, input_type, input_group, input_col], \n        outputs=output\n    )\n\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001, clipnorm=1.0),\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy',\n                 tf.keras.metrics.SparseTopKCategoricalAccuracy(k=12, name='top_12_accuracy'),\n                 tf.keras.metrics.SparseTopKCategoricalAccuracy(k=5, name='top_5_accuracy')]\n    )\n    \n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:41:06.512654Z","iopub.execute_input":"2026-01-02T10:41:06.512974Z","iopub.status.idle":"2026-01-02T10:41:06.520984Z","shell.execute_reply.started":"2026-01-02T10:41:06.512947Z","shell.execute_reply":"2026-01-02T10:41:06.520225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"strategy = tf.distribute.MirroredStrategy()\nwith strategy.scope():\n    model_feat = build_multi_input_model() \n    model_feat.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:41:13.941134Z","iopub.execute_input":"2026-01-02T10:41:13.941934Z","iopub.status.idle":"2026-01-02T10:41:14.187191Z","shell.execute_reply.started":"2026-01-02T10:41:13.941900Z","shell.execute_reply":"2026-01-02T10:41:14.186628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"early_stop = tf.keras.callbacks.EarlyStopping(\n    monitor='val_loss', \n    patience=3,           \n    restore_best_weights=True # Зберігаємо найкращу версію\n)\n\nhistory_feat = model_feat.fit(\n    train_loader, \n    validation_data=test_loader,\n    epochs=20,\n    callbacks=[early_stop]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T10:41:14.188693Z","iopub.execute_input":"2026-01-02T10:41:14.188968Z","iopub.status.idle":"2026-01-02T11:14:40.234876Z","shell.execute_reply.started":"2026-01-02T10:41:14.188947Z","shell.execute_reply":"2026-01-02T11:14:40.234262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_history(history_feat)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:14:40.235891Z","iopub.execute_input":"2026-01-02T11:14:40.236166Z","iopub.status.idle":"2026-01-02T11:14:40.764358Z","shell.execute_reply.started":"2026-01-02T11:14:40.236143Z","shell.execute_reply":"2026-01-02T11:14:40.763812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_feat.save('hm_lstm_multi_input_model.keras')\ndel model_feat","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:14:40.765149Z","iopub.execute_input":"2026-01-02T11:14:40.765434Z","iopub.status.idle":"2026-01-02T11:14:45.290525Z","shell.execute_reply.started":"2026-01-02T11:14:40.765403Z","shell.execute_reply":"2026-01-02T11:14:45.289886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"loaded_model_mi = load_model('hm_lstm_multi_input_model.keras')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:14:45.294607Z","iopub.execute_input":"2026-01-02T11:14:45.294818Z","iopub.status.idle":"2026-01-02T11:14:50.357379Z","shell.execute_reply.started":"2026-01-02T11:14:45.294798Z","shell.execute_reply":"2026-01-02T11:14:50.356575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tf.keras.utils.plot_model(\n    loaded_model_mi, \n    show_shapes=True, \n    show_layer_names=True,\n    rankdir='TB', # 'TB' for Top-to-Bottom, 'LR' for Left-to-Right\n    expand_nested=True,\n    dpi=96\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:14:50.358360Z","iopub.execute_input":"2026-01-02T11:14:50.358600Z","iopub.status.idle":"2026-01-02T11:14:50.774617Z","shell.execute_reply.started":"2026-01-02T11:14:50.358578Z","shell.execute_reply":"2026-01-02T11:14:50.773896Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"top_12_indices_mi = create_predict(loaded_model_mi, test_loader, model_type='multi_input')\nlstm_multi_input_score = map12_score(top_12_indices_mi, y_test)\nprint(lstm_multi_input_score)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:14:50.775500Z","iopub.execute_input":"2026-01-02T11:14:50.775790Z","iopub.status.idle":"2026-01-02T11:15:32.653456Z","shell.execute_reply.started":"2026-01-02T11:14:50.775760Z","shell.execute_reply":"2026-01-02T11:15:32.652692Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Results","metadata":{}},{"cell_type":"code","source":"print(\"MAP@12\")\nprint(f\"\\nBaseline (Last 12 items): {baseline_score:.6f}\")\nprint(f\"LSTM with features: {lstm_multi_input_score:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-02T11:15:32.654482Z","iopub.execute_input":"2026-01-02T11:15:32.654759Z","iopub.status.idle":"2026-01-02T11:15:32.658921Z","shell.execute_reply.started":"2026-01-02T11:15:32.654736Z","shell.execute_reply":"2026-01-02T11:15:32.658368Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"* Baseline (Last 12 items): 0.013672\n* LSTM with features: 0.022937\n* LSTM with features and attention layer: 0.017845","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}