{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":10737,"databundleVersionId":290346}],"dockerImageVersionId":31287,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":2531.457782,"end_time":"2026-03-04T14:48:30.071442","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-03-04T14:06:18.613660","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"fdd86a61","cell_type":"markdown","source":"# ДЗ3 - Обработка естественного языка (NLP)\n## Соревнование - Quora Insincere Questions Classification\n(https://www.kaggle.com/competitions/quora-insincere-questions-classification\n)\n\nВыполнил - Фахретдинов Артур\n\nПостановка задачи - Построить модель, которая по тексту вопроса на платформе Quora определяет, является ли он искренним или содержит токсичный, провокационный или вводящий в заблуждение смысл, чтобы помочь автоматически выявлять и фильтровать некорректный контент.","metadata":{"papermill":{"duration":0.003831,"end_time":"2026-03-04T14:06:21.225745","exception":false,"start_time":"2026-03-04T14:06:21.221914","status":"completed"},"tags":[]}},{"id":"13a8d116","cell_type":"markdown","source":"Идея решения:\n\n1. Проведу базовую предобработку текста (приведение к нижнему регистру, очистка и токенизация).\n\n2. Построю признаки на основе TF-IDF представления текста, используя как word n-grams, так и char n-grams, чтобы учитывать как смысловые конструкции, так и характерные текстовые паттерны.\n\n3. В качестве базовой гипотезы обучу линейную модель Logistic Regression, которая хорошо работает с разреженными TF-IDF признаками и позволяет быстро получить устойчивый baseline.\n\n4. В качестве второй гипотезы построю нейросетевую модель (BiGRU) с использованием предобученных word embeddings, предоставленных в рамках соревнования, чтобы учитывать контекст и семантические связи между словами.\n\n5. Обучение моделей проведу с использованием cross-validation, а также подберу оптимальный порог классификации, так как метрика соревнования — F1-score.\n\n6. После обучения выполню инференс на тестовом наборе и сравню значения CV и leaderboard, чтобы проанализировать возможный CV–LB gap и устойчивость моделей.","metadata":{"papermill":{"duration":0.002813,"end_time":"2026-03-04T14:06:21.231587","exception":false,"start_time":"2026-03-04T14:06:21.228774","status":"completed"},"tags":[]}},{"id":"955c59dd","cell_type":"code","source":"import os, re, gc, math, random\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import f1_score\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom scipy.sparse import hstack, csr_matrix\n\nimport torch\nfrom tqdm import tqdm\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2026-03-05T08:32:02.823181Z","iopub.execute_input":"2026-03-05T08:32:02.823898Z","iopub.status.idle":"2026-03-05T08:32:11.082931Z","shell.execute_reply.started":"2026-03-05T08:32:02.823867Z","shell.execute_reply":"2026-03-05T08:32:11.082072Z"},"papermill":{"duration":5.861867,"end_time":"2026-03-04T14:06:27.096461","exception":false,"start_time":"2026-03-04T14:06:21.234594","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"606f4c6b","cell_type":"code","source":"def set_seeds(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nset_seeds(42)","metadata":{"execution":{"iopub.status.busy":"2026-03-05T08:32:11.084365Z","iopub.execute_input":"2026-03-05T08:32:11.084820Z","iopub.status.idle":"2026-03-05T08:32:11.097615Z","shell.execute_reply.started":"2026-03-05T08:32:11.084794Z","shell.execute_reply":"2026-03-05T08:32:11.096927Z"},"papermill":{"duration":0.013549,"end_time":"2026-03-04T14:06:27.113123","exception":false,"start_time":"2026-03-04T14:06:27.099574","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"57238f12","cell_type":"code","source":"DATA_DIR = \"/kaggle/input/competitions/quora-insincere-questions-classification\"\ntrain_path = os.path.join(DATA_DIR, \"train.csv\")\ntest_path  = os.path.join(DATA_DIR, \"test.csv\")\nsub_path   = os.path.join(DATA_DIR, \"sample_submission.csv\")\n\ntrain_df = pd.read_csv(train_path)\ntest_df  = pd.read_csv(test_path)\nsub_df   = pd.read_csv(sub_path)\n\nprint(train_df.shape, test_df.shape)\ntrain_df.head()","metadata":{"execution":{"iopub.status.busy":"2026-03-05T08:32:11.098523Z","iopub.execute_input":"2026-03-05T08:32:11.098758Z","iopub.status.idle":"2026-03-05T08:32:16.271250Z","shell.execute_reply.started":"2026-03-05T08:32:11.098738Z","shell.execute_reply":"2026-03-05T08:32:16.270395Z"},"papermill":{"duration":4.466932,"end_time":"2026-03-04T14:06:31.582935","exception":false,"start_time":"2026-03-04T14:06:27.116003","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"20b03e25","cell_type":"markdown","source":"Чистим текст","metadata":{"papermill":{"duration":0.003279,"end_time":"2026-03-04T14:06:31.589916","exception":false,"start_time":"2026-03-04T14:06:31.586637","status":"completed"},"tags":[]}},{"id":"a6a59a76","cell_type":"code","source":"def clean_text(s: str) -> str:\n    s = str(s).lower()\n    s = re.sub(r\"\\s+\", \" \", s).strip()\n    return s\n\ntrain_text = train_df[\"question_text\"].fillna(\"\").map(clean_text).values\ntest_text  = test_df[\"question_text\"].fillna(\"\").map(clean_text).values\ny = train_df[\"target\"].values.astype(int)","metadata":{"execution":{"iopub.status.busy":"2026-03-05T08:32:16.272949Z","iopub.execute_input":"2026-03-05T08:32:16.273192Z","iopub.status.idle":"2026-03-05T08:32:25.560787Z","shell.execute_reply.started":"2026-03-05T08:32:16.273170Z","shell.execute_reply":"2026-03-05T08:32:25.560161Z"},"papermill":{"duration":9.964784,"end_time":"2026-03-04T14:06:41.557959","exception":false,"start_time":"2026-03-04T14:06:31.593175","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1f6ee41c","cell_type":"markdown","source":"Проверяем первую гипотезу, строим Tf-idf и далее Logreg","metadata":{"papermill":{"duration":0.003466,"end_time":"2026-03-04T14:06:41.565131","exception":false,"start_time":"2026-03-04T14:06:41.561665","status":"completed"},"tags":[]}},{"id":"5f727277","cell_type":"code","source":"word_vec = TfidfVectorizer(\n    ngram_range=(1,2),\n    min_df=3,\n    max_df=0.9,\n    strip_accents=\"unicode\",\n    analyzer=\"word\",\n    sublinear_tf=True\n)\n\nchar_vec = TfidfVectorizer(\n    ngram_range=(3,5),\n    min_df=3,\n    max_df=0.9,\n    strip_accents=\"unicode\",\n    analyzer=\"char\",\n    sublinear_tf=True\n)\n\nX_word = word_vec.fit_transform(train_text)\nX_char = char_vec.fit_transform(train_text)\nX = hstack([X_word, X_char]).tocsr()\n\nX_test_word = word_vec.transform(test_text)\nX_test_char = char_vec.transform(test_text)\nX_test = hstack([X_test_word, X_test_char]).tocsr()\n\nprint(X.shape, X_test.shape)","metadata":{"execution":{"iopub.status.busy":"2026-03-05T08:32:25.561838Z","iopub.execute_input":"2026-03-05T08:32:25.562076Z"},"papermill":{"duration":295.28426,"end_time":"2026-03-04T14:11:36.852671","exception":false,"start_time":"2026-03-04T14:06:41.568411","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9a8257be","cell_type":"code","source":"def find_best_threshold(y_true, prob, n_steps=101):\n    best_t, best_f1 = 0.5, -1\n    for t in np.linspace(0.05, 0.95, n_steps):\n        pred = (prob >= t).astype(int)\n        f1 = f1_score(y_true, pred)\n        if f1 > best_f1:\n            best_f1 = f1\n            best_t = float(t)\n    return best_t, float(best_f1)\n\ndef cv_logreg_tfidf(X, y, X_test, n_splits=5, seed=42):\n    skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=seed)\n\n    test_prob = np.zeros(X_test.shape[0], dtype=np.float32)\n\n    thresholds = []\n    f1s = []\n\n    for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), 1):\n        print(f\"\\n[H0] Fold {fold}/{n_splits}\")\n\n        X_train, X_val = X[train_idx], X[val_idx]\n        y_train, y_val = y[train_idx], y[val_idx]\n\n        model = LogisticRegression(\n            solver=\"liblinear\",\n            C=4.0,\n            max_iter=200,\n        )\n        model.fit(X_train, y_train)\n\n        val_prob = model.predict_proba(X_val)[:, 1]\n        t, f1 = find_best_threshold(y_val, val_prob)\n        thresholds.append(t)\n        f1s.append(f1)\n        print(f\" fold best_th={t:.3f} | fold_f1={f1:.5f}\")\n\n        test_prob += model.predict_proba(X_test)[:, 1] / n_splits\n\n    t_global = float(np.median(thresholds))\n\n    print(\"\\n[H0] CV summary\")\n    print(\" fold_f1:\", [round(x,5) for x in f1s])\n    print(\" median_threshold:\", t_global)\n\n    return test_prob, t_global\n\ntest_prob0, th0 = cv_logreg_tfidf(X, y, X_test, n_splits=5, seed=42)","metadata":{"papermill":{"duration":0.013863,"end_time":"2026-03-04T14:11:36.869778","exception":false,"start_time":"2026-03-04T14:11:36.855915","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ea8d5e91","cell_type":"code","source":"pred0 = (test_prob0 >= th0).astype(int)\nsub0 = pd.DataFrame({\"qid\": test_df[\"qid\"], \"prediction\": pred0})\nsub0.to_csv(\"submission.csv\", index=False)\nsub0.head()","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:11:36.877671Z","iopub.status.busy":"2026-03-04T14:11:36.877078Z","iopub.status.idle":"2026-03-04T14:11:36.880405Z","shell.execute_reply":"2026-03-04T14:11:36.879655Z"},"papermill":{"duration":0.0088,"end_time":"2026-03-04T14:11:36.881818","exception":false,"start_time":"2026-03-04T14:11:36.873018","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"0e21dc0b-f515-47a4-8cd6-9271a457eebe","cell_type":"markdown","source":"Получили:\n\nPrivate score - 0.656, Public - 0.649\n\nПолученный результат показывает относительно небольшой разрыв между Public и Private leaderboard, что говорит о достаточно стабильной работе модели и отсутствии сильного переобучения на публичную часть тестового набора.\n\nНесмотря на простоту подхода, комбинация TF-IDF представления текста и линейной модели Logistic Regression показала хороший baseline результат. Такой подход хорошо работает с разреженными текстовыми признаками и способен эффективно выявлять характерные n-граммы и текстовые паттерны, связанные с токсичными или провокационными вопросами.\n\nНебольшое улучшение результата на private части может свидетельствовать о хорошей обобщающей способности модели и корректно выбранной стратегии cross-validation и подбора порога для оптимизации метрики F1-score.\n\nТаким образом, полученная модель выступает в роли надёжного baseline решения, на основе которого можно дальше улучшать качество за счёт более сложных моделей обработки текста, например нейросетевых архитектур с использованием предобученных эмбеддингов.","metadata":{}},{"id":"4ff7d78d","cell_type":"markdown","source":"Теперь проверяем вторую гипотезу, строим BiGRU модель","metadata":{"papermill":{"duration":0.003249,"end_time":"2026-03-04T14:11:36.888125","exception":false,"start_time":"2026-03-04T14:11:36.884876","status":"completed"},"tags":[]}},{"id":"f1e9c2df","cell_type":"code","source":"from collections import Counter\n\n\ndef simple_tokenize(s: str):\n    return re.findall(r\"[a-z0-9]+|[^\\s\\w]\", s.lower())\n\ncounter = Counter()\nfor txt in train_text:\n    counter.update(simple_tokenize(txt))\n\nMAX_VOCAB = 120000\nMIN_FREQ = 2\n\nmost_common = [w for w, c in counter.most_common(MAX_VOCAB) if c >= MIN_FREQ]\n\nPAD = \"<pad>\"\nUNK = \"<unk>\"\nword2idx = {PAD: 0, UNK: 1}\nfor w in most_common:\n    if w not in word2idx:\n        word2idx[w] = len(word2idx)\n\nidx2word = {i:w for w,i in word2idx.items()}\nvocab_size = len(word2idx)\nprint(\"vocab_size:\", vocab_size)","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:11:36.895863Z","iopub.status.busy":"2026-03-04T14:11:36.895321Z","iopub.status.idle":"2026-03-04T14:11:47.312717Z","shell.execute_reply":"2026-03-04T14:11:47.311762Z"},"papermill":{"duration":10.423128,"end_time":"2026-03-04T14:11:47.314477","exception":false,"start_time":"2026-03-04T14:11:36.891349","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"67a36a0c","cell_type":"code","source":"MAX_LEN = 72\n\ndef text_to_ids(txt: str, word2idx, max_len=72):\n    toks = simple_tokenize(txt)\n    ids = [word2idx.get(t, 1) for t in toks][:max_len]\n    if len(ids) < max_len:\n        ids += [0] * (max_len - len(ids))\n    return ids\n\nX_ids = np.vstack([text_to_ids(t, word2idx, MAX_LEN) for t in train_text]).astype(np.int64)\nT_ids = np.vstack([text_to_ids(t, word2idx, MAX_LEN) for t in test_text]).astype(np.int64)\n\nprint(X_ids.shape, T_ids.shape)","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:11:47.323387Z","iopub.status.busy":"2026-03-04T14:11:47.322716Z","iopub.status.idle":"2026-03-04T14:12:18.653504Z","shell.execute_reply":"2026-03-04T14:12:18.652550Z"},"papermill":{"duration":31.340121,"end_time":"2026-03-04T14:12:18.658424","exception":false,"start_time":"2026-03-04T14:11:47.318303","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"bbb83e9a","cell_type":"code","source":"!unzip /kaggle/input/competitions/quora-insincere-questions-classification/embeddings.zip","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:12:18.667165Z","iopub.status.busy":"2026-03-04T14:12:18.666315Z","iopub.status.idle":"2026-03-04T14:15:08.368450Z","shell.execute_reply":"2026-03-04T14:15:08.367576Z"},"papermill":{"duration":169.708674,"end_time":"2026-03-04T14:15:08.370504","exception":false,"start_time":"2026-03-04T14:12:18.661830","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"145b05ac","cell_type":"code","source":"EMB_PATH = \"/kaggle/working/paragram_300_sl999/paragram_300_sl999.txt\"\nEMB_DIM = 300\n\ndef load_embedding_matrix(emb_path, word2idx, emb_dim=300, max_words=None):\n    vocab = set(word2idx.keys())\n    matrix = np.random.normal(0, 0.02, size=(len(word2idx), emb_dim)).astype(np.float32)\n    matrix[word2idx[\"<pad>\"]] = 0.0\n\n    found = 0\n    with open(emb_path, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:\n        for line in f:\n            parts = line.rstrip().split(\" \")\n            if len(parts) <= emb_dim:\n                continue\n            word = parts[0]\n            if word in vocab:\n                vec = np.asarray(parts[-emb_dim:], dtype=np.float32)\n                matrix[word2idx[word]] = vec\n                found += 1\n            if max_words is not None and found >= max_words:\n                break\n\n    print(f\"Loaded embeddings: {found}/{len(word2idx)}\")\n    return matrix\n\nemb_matrix = load_embedding_matrix(EMB_PATH, word2idx, emb_dim=EMB_DIM)","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:15:08.379703Z","iopub.status.busy":"2026-03-04T14:15:08.379393Z","iopub.status.idle":"2026-03-04T14:15:46.061430Z","shell.execute_reply":"2026-03-04T14:15:46.060587Z"},"papermill":{"duration":37.692673,"end_time":"2026-03-04T14:15:46.067002","exception":false,"start_time":"2026-03-04T14:15:08.374329","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"58132e52","cell_type":"code","source":"class SeqDataset(Dataset):\n    def __init__(self, X_ids, y=None):\n        self.X = torch.tensor(X_ids, dtype=torch.long)\n        self.y = None if y is None else torch.tensor(y, dtype=torch.float32)\n\n    def __len__(self):\n        return self.X.size(0)\n\n    def __getitem__(self, idx):\n        if self.y is None:\n            return self.X[idx]\n        return self.X[idx], self.y[idx]\n\nclass BiGRU(nn.Module):\n    def __init__(self, vocab_size, emb_dim, hidden=128, n_layers=1, dropout=0.2, emb_matrix=None):\n        super().__init__()\n        self.embedding = nn.Embedding(vocab_size, emb_dim, padding_idx=0)\n        if emb_matrix is not None:\n            self.embedding.weight.data.copy_(torch.tensor(emb_matrix))\n        self.embedding.weight.requires_grad = True\n\n        self.gru = nn.GRU(\n            emb_dim,\n            hidden,\n            num_layers=n_layers,\n            batch_first=True,\n            bidirectional=True\n        )\n\n        self.dropout = nn.Dropout(dropout)\n        self.fc = nn.Linear(hidden * 2, 1)\n\n    def forward(self, x):\n        emb = self.embedding(x)\n        out, _ = self.gru(emb)\n\n        out = torch.max(out, dim=1).values\n        out = self.dropout(out)\n        logits = self.fc(out).squeeze(1)\n        return logits","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:15:46.075935Z","iopub.status.busy":"2026-03-04T14:15:46.075635Z","iopub.status.idle":"2026-03-04T14:15:46.084443Z","shell.execute_reply":"2026-03-04T14:15:46.083764Z"},"papermill":{"duration":0.015156,"end_time":"2026-03-04T14:15:46.086051","exception":false,"start_time":"2026-03-04T14:15:46.070895","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"89faf85d","cell_type":"code","source":"@torch.no_grad()\ndef predict_nn(model, loader, device):\n    model.eval()\n    probs = []\n    for batch in loader:\n        if isinstance(batch, (list, tuple)) and len(batch) == 2:\n            x, _ = batch\n        else:\n            x = batch\n        x = x.to(device)\n        logits = model(x)\n        p = torch.sigmoid(logits).detach().cpu().numpy()\n        probs.append(p)\n    return np.concatenate(probs).reshape(-1)\n\ndef train_epoch_nn(model, loader, optimizer, device):\n    model.train()\n    loss_fn = nn.BCEWithLogitsLoss()\n    total = 0.0\n    for x, yb in tqdm(loader, leave=False):\n        x = x.to(device)\n        yb = yb.to(device)\n\n        optimizer.zero_grad(set_to_none=True)\n        logits = model(x)\n        loss = loss_fn(logits, yb)\n        loss.backward()\n        optimizer.step()\n        total += loss.item() * x.size(0)\n    return total / len(loader.dataset)\n\n@torch.no_grad()\ndef valid_epoch_nn(model, loader, device):\n    model.eval()\n    loss_fn = nn.BCEWithLogitsLoss()\n    total = 0.0\n    ys = []\n    probs = []\n    for x, yb in tqdm(loader, leave=False):\n        x = x.to(device)\n        yb = yb.to(device)\n        logits = model(x)\n        loss = loss_fn(logits, yb)\n        p = torch.sigmoid(logits)\n\n        total += loss.item() * x.size(0)\n        ys.append(yb.detach().cpu().numpy())\n        probs.append(p.detach().cpu().numpy())\n    ys = np.concatenate(ys).reshape(-1)\n    probs = np.concatenate(probs).reshape(-1)\n    return total / len(loader.dataset), ys, probs","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:15:46.094901Z","iopub.status.busy":"2026-03-04T14:15:46.094608Z","iopub.status.idle":"2026-03-04T14:15:46.104284Z","shell.execute_reply":"2026-03-04T14:15:46.103460Z"},"papermill":{"duration":0.016017,"end_time":"2026-03-04T14:15:46.105920","exception":false,"start_time":"2026-03-04T14:15:46.089903","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8896d53f","cell_type":"code","source":"def cv_bigru(\n    X_ids, y, T_ids,\n    emb_matrix,\n    n_splits=5,\n    seed=42,\n    epochs=4,\n    batch_size=512,\n    lr=1e-3\n):\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=seed)\n\n    test_prob = np.zeros(T_ids.shape[0], dtype=np.float32)\n\n    thresholds = []\n    f1s = []\n\n    for fold, (train_idx, val_idx) in enumerate(skf.split(X_ids, y), 1):\n        print(f\"\\n[H1] Fold {fold}/{n_splits}\")\n\n        ds_train = SeqDataset(X_ids[train_idx], y[train_idx])\n        ds_val = SeqDataset(X_ids[val_idx], y[val_idx])\n        ds_test = SeqDataset(T_ids, None)\n\n        dl_train = DataLoader(ds_train, batch_size=batch_size, shuffle=True, num_workers=0)\n        dl_val = DataLoader(ds_val, batch_size=batch_size, shuffle=False, num_workers=0)\n        dl_test = DataLoader(ds_test, batch_size=batch_size, shuffle=False, num_workers=0)\n\n        model = BiGRU(\n            vocab_size=vocab_size,\n            emb_dim=EMB_DIM,\n            hidden=128,\n            dropout=0.2,\n            emb_matrix=emb_matrix\n        ).to(device)\n\n        optimizer = torch.optim.Adam(model.parameters(), lr=lr)\n\n        best_f1 = -1\n        best_state = None\n        best_th = 0.5\n\n        for ep in range(1, epochs + 1):\n            tr_loss = train_epoch_nn(model, dl_train, optimizer, device)\n            va_loss, va_y, val_prob = valid_epoch_nn(model, dl_val, device)\n\n            th, f1 = find_best_threshold(va_y, val_prob, n_steps=81)\n            print(f\" epoch {ep} | train_loss={tr_loss:.4f} | val_loss={va_loss:.4f} | best_th={th:.3f} | f1={f1:.5f}\")\n\n            if f1 > best_f1:\n                best_f1 = f1\n                best_th = th\n                best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}\n\n        model.load_state_dict(best_state)\n\n        te_prob = predict_nn(model, dl_test, device)\n        test_prob += te_prob / n_splits\n\n        thresholds.append(best_th)\n        f1s.append(best_f1)\n        print(f\" fold best_f1={best_f1:.5f} | fold_th={best_th:.3f}\")\n\n        del model, ds_train, ds_val, ds_test, dl_train, dl_val, dl_test\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    th_global = float(np.median(thresholds))\n\n\n    print(\"\\n[H1] CV summary\")\n    print(\" fold_f1:\", [round(x,5) for x in f1s])\n    print(\" median_threshold:\", th_global)\n\n    return test_prob, th_global\n\ntest_prob1, th1 = cv_bigru(\n    X_ids, y, T_ids,\n    emb_matrix=emb_matrix,\n    n_splits=5,\n    epochs=2,\n    batch_size=512,\n    lr=1e-3\n)","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:15:46.114756Z","iopub.status.busy":"2026-03-04T14:15:46.114487Z","iopub.status.idle":"2026-03-04T14:48:24.367607Z","shell.execute_reply":"2026-03-04T14:48:24.366795Z"},"papermill":{"duration":1958.259728,"end_time":"2026-03-04T14:48:24.369413","exception":false,"start_time":"2026-03-04T14:15:46.109685","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"9330c4a4","cell_type":"code","source":"pred1 = (test_prob1 >= th1).astype(int)\nsub1 = pd.DataFrame({\"qid\": test_df[\"qid\"], \"prediction\": pred1})\nsub1.to_csv(\"submission.csv\", index=False)\nsub1.head()","metadata":{"execution":{"iopub.execute_input":"2026-03-04T14:48:25.532163Z","iopub.status.busy":"2026-03-04T14:48:25.531187Z","iopub.status.idle":"2026-03-04T14:48:25.952612Z","shell.execute_reply":"2026-03-04T14:48:25.951940Z"},"papermill":{"duration":1.003463,"end_time":"2026-03-04T14:48:25.954282","exception":false,"start_time":"2026-03-04T14:48:24.950819","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f157a32d-48ef-460e-b7b6-cbea1a7fdc15","cell_type":"markdown","source":"Получили:\n\nPrivate score - 0.685, Public - 0.676\n\nНейросетевая модель BiGRU с предобученными эмбеддингами показала более высокое качество по сравнению с базовым решением на TF-IDF и Logistic Regression. Это говорит о том, что модель лучше улавливает контекст и семантические связи между словами в вопросах.\n\nРазрыв между Public и Private leaderboard остаётся небольшим, что свидетельствует о стабильности модели и отсутствии сильного переобучения. В целом, использование нейросетевой архитектуры позволило улучшить результат и подтвердило гипотезу о том, что модели, учитывающие контекст текста, могут эффективнее решать задачу классификации неискренних вопросов.\n","metadata":{}}]}