{"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":"code","source":"import numpy as np\nimport pandas as pd\nfrom tqdm.notebook import tqdm\nnp.random.seed(42)\n\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:46:23.695565Z","iopub.execute_input":"2025-12-23T10:46:23.696223Z","iopub.status.idle":"2025-12-23T10:46:27.349138Z","shell.execute_reply.started":"2025-12-23T10:46:23.696184Z","shell.execute_reply":"2025-12-23T10:46:27.348526Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"###  SRC","metadata":{}},{"cell_type":"code","source":"def map12(pred_items, target_item):\n    \"\"\"\n    pred_items : список з 12 рекомендованих item_id (в порядку від 1 до 12)\n    target_item : правильний item_id\n    \"\"\"\n    score = 0.0\n    for k in range(1, 13):\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)\n\ndef plot_history(history):\n    plt.figure(figsize=(12, 5))\n\n    # Графік Loss (Втрат)\n    plt.subplot(1, 2, 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    # Графік Accuracy (Точності)\n    plt.subplot(1, 2, 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.tight_layout()\n    plt.show()\n\n\ndef 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":"2025-12-23T10:46:27.350244Z","iopub.execute_input":"2025-12-23T10:46:27.350761Z","iopub.status.idle":"2025-12-23T10:46:27.359191Z","shell.execute_reply.started":"2025-12-23T10:46:27.350721Z","shell.execute_reply":"2025-12-23T10:46:27.358600Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training","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()\n\ntop_users = purchase_counts.sort_values(ascending=False).head(10000).index.tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:46:27.629431Z","iopub.execute_input":"2025-12-23T10:46:27.629729Z","iopub.status.idle":"2025-12-23T10:47:24.377279Z","shell.execute_reply.started":"2025-12-23T10:46:27.629705Z","shell.execute_reply":"2025-12-23T10:47:24.376367Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"unique_transactions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:47:24.378643Z","iopub.execute_input":"2025-12-23T10:47:24.378970Z","iopub.status.idle":"2025-12-23T10:47:24.390501Z","shell.execute_reply.started":"2025-12-23T10:47:24.378940Z","shell.execute_reply":"2025-12-23T10:47:24.389850Z"}},"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":"2025-12-23T10:47:30.438043Z","iopub.execute_input":"2025-12-23T10:47:30.438849Z","iopub.status.idle":"2025-12-23T10:47:30.723664Z","shell.execute_reply.started":"2025-12-23T10:47:30.438818Z","shell.execute_reply":"2025-12-23T10:47:30.722783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_top = unique_transactions[unique_transactions.customer_id.isin(top_users)].reset_index(drop=True)\n\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":"2025-12-23T10:47:32.414143Z","iopub.execute_input":"2025-12-23T10:47:32.414439Z","iopub.status.idle":"2025-12-23T10:47:36.960865Z","shell.execute_reply.started":"2025-12-23T10:47:32.414414Z","shell.execute_reply":"2025-12-23T10:47:36.960152Z"}},"outputs":[],"execution_count":null},{"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)\n\ntrain_size = int(0.9 * len(top_users_le))\n\ntrain_users = set(top_users_le[:train_size])\ntest_users  = set(top_users_le[train_size:])\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(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":"2025-12-23T10:47:39.732803Z","iopub.execute_input":"2025-12-23T10:47:39.733592Z","iopub.status.idle":"2025-12-23T10:47:40.422103Z","shell.execute_reply.started":"2025-12-23T10:47:39.733560Z","shell.execute_reply":"2025-12-23T10:47:40.421516Z"}},"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":"2025-12-23T10:47:45.626895Z","iopub.execute_input":"2025-12-23T10:47:45.627532Z","iopub.status.idle":"2025-12-23T10:47:49.801608Z","shell.execute_reply.started":"2025-12-23T10:47:45.627502Z","shell.execute_reply":"2025-12-23T10:47:49.800800Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\n\nBATCH_SIZE = 1024*4\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":"2025-12-23T10:47:49.802927Z","iopub.execute_input":"2025-12-23T10:47:49.803176Z","iopub.status.idle":"2025-12-23T10:47:50.438032Z","shell.execute_reply.started":"2025-12-23T10:47:49.803150Z","shell.execute_reply":"2025-12-23T10:47:50.437210Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# next(iter(train_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:47:50.439017Z","iopub.execute_input":"2025-12-23T10:47:50.439434Z","iopub.status.idle":"2025-12-23T10:47:50.443930Z","shell.execute_reply.started":"2025-12-23T10:47:50.439402Z","shell.execute_reply":"2025-12-23T10:47:50.443332Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.layers import Input, Embedding, LSTM, Dense, Dropout, BatchNormalization, Concatenate\nfrom tensorflow.keras.models import Model\n\ndef 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    # 2. Embedding шари для кожної фічі\n    # Для артикулів залишаємо більше векторів (64), для категорій — менше (16-32)\n    emb_art = Embedding(article_vocab_size, 64, name='emb_art')(input_art)\n    emb_type = Embedding(prod_type_vocab_size, 16, name='emb_type')(input_type)\n    emb_group = Embedding(prod_group_vocab_size, 16, name='emb_group')(input_group)\n    emb_col = Embedding(color_vocab_size, 16, name='emb_col')(input_col)\n\n    # 3. Конкатенація (об'єднуємо всі ознаки в один вектор для кожного кроку часу)\n    # Тепер на кожному з 50 кроків LSTM бачитиме вектор розмірністю 64+16+16+16 = 112\n    merged = Concatenate()([emb_art, emb_type, emb_group, emb_col])\n\n    # 4. Рекурентна частина\n    lstm_out = LSTM(128, dropout=0.4)(merged)\n    \n    # 5. Повнозв'язні шари\n    x = BatchNormalization()(lstm_out)\n    x = Dense(256, activation='relu')(x)\n    x = Dropout(0.5)(x)\n    \n    # 6. Вихідний шар (передбачаємо тільки ID наступного артикула)\n    output = Dense(article_vocab_size, activation='softmax', name='output')(x)\n\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),\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy']\n    )\n    \n    return model\n\n# Створюємо модель\n# strategy = tf.distribute.MirroredStrategy()\n# with strategy.scope():\nmodel = build_multi_input_model() # Твоя функція для Functional API\nmodel.summary()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Налаштування EarlyStopping, щоб не чекати всі 20 епох, якщо модель перестане вчитися\nearly_stop = tf.keras.callbacks.EarlyStopping(\n    monitor='val_loss', \n    patience=3,           # Чекаємо 3 епохи без покращень\n    restore_best_weights=True # Зберігаємо найкращу версію\n)\n\n# Навчання на розширеному (augmented) датасеті\n# Якщо використовуєш DataLoader (рекомендую):\nhistory = model.fit(\n    train_loader, # DataLoader сам розбереться з іменами входів\n    validation_data=test_loader,\n    epochs=20,\n    callbacks=[early_stop]\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.save('hm_lstm_multi_input_model.keras')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.models import load_model\n\n# Вказуємо шлях до другого GPU (індексація починається з 0)\n\nloaded_model = load_model('hm_lstm_multi_input_model.keras')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:48:17.527044Z","iopub.execute_input":"2025-12-23T10:48:17.527343Z","iopub.status.idle":"2025-12-23T10:48:19.622066Z","shell.execute_reply.started":"2025-12-23T10:48:17.527317Z","shell.execute_reply":"2025-12-23T10:48:19.621291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom tqdm import tqdm\nimport tensorflow as tf\n\nall_top_12 = []\n\n# Ітеруємося по батчах з DataLoader\nfor x_batch, _ in tqdm(test_loader, desc=\"Predicting batches\"):\n    # Отримуємо ймовірності тільки для поточного батчу (наприклад, 256 юзерів)\n    batch_preds = loaded_model.predict_on_batch(x_batch)\n    \n    # Одразу знаходимо ТОП-12 індексів (найбільші ймовірності)\n    # np.argsort дає індекси від найменшого до найбільшого\n    # [:, -12:] бере 12 найкращих, [:, ::-1] розвертає їх у правильному порядку\n    batch_top_12 = np.argsort(batch_preds, axis=1)[:, -12:][:, ::-1]\n    \n    # Зберігаємо лише ці 12 чисел на кожного юзера\n    all_top_12.append(batch_top_12)\n\n# 3. Об'єднуємо всі батчі в один масив\ntop_12_indices = np.vstack(all_top_12)\n\nprint(f\"\\nГотово! Отримано прогнозів для {top_12_indices.shape[0]} користувачів.\")\nprint(f\"Формат виходу: {top_12_indices.shape}\") # Має бути (кількість_юзерів, 12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:48:26.410024Z","iopub.execute_input":"2025-12-23T10:48:26.410678Z","iopub.status.idle":"2025-12-23T10:49:43.702996Z","shell.execute_reply.started":"2025-12-23T10:48:26.410636Z","shell.execute_reply":"2025-12-23T10:49:43.702170Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_history(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:50:42.820122Z","iopub.execute_input":"2025-12-23T10:50:42.820674Z","iopub.status.idle":"2025-12-23T10:50:42.826199Z","shell.execute_reply.started":"2025-12-23T10:50:42.820637Z","shell.execute_reply":"2025-12-23T10:50:42.825257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"lstm_score = map12_score(top_12_indices, y_test)\nprint(f\"LSTM Validation MAP@12: {lstm_score:.6f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-23T10:50:10.980439Z","iopub.execute_input":"2025-12-23T10:50:10.981011Z","iopub.status.idle":"2025-12-23T10:50:11.097558Z","shell.execute_reply.started":"2025-12-23T10:50:10.980981Z","shell.execute_reply":"2025-12-23T10:50:11.096948Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"map_scores = []\n\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(map12(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":"2025-12-23T10:50:33.906406Z","iopub.execute_input":"2025-12-23T10:50:33.906983Z","iopub.status.idle":"2025-12-23T10:50:33.997161Z","shell.execute_reply.started":"2025-12-23T10:50:33.906954Z","shell.execute_reply":"2025-12-23T10:50:33.996345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}