Загрузка данных
# ============================================================
# МТС "Умная полка" Track A
# Быстрый демонстрационный режим обучения ~ до 10 минут
# Скачивание zip из облака -> обучение -> pred_a_fast.csv
# ============================================================
import os
import sys
import subprocess
import zipfile
import random
from pathlib import Path
# ----------------------------
# 1. Установка библиотек
# ----------------------------
packages = [
"timm",
"opencv-python-headless",
"scikit-learn",
"tqdm",
"requests"
]
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-q"] + packages
)
# ----------------------------
# 2. Импорты
# ----------------------------
import requests
import numpy as np
import pandas as pd
import cv2
from tqdm.auto import tqdm
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import timm
from sklearn.model_selection import StratifiedGroupKFold
from google.colab import files
# ----------------------------
# 3. Настройки быстрого режима
# ----------------------------
DATA_URL = "https://storage.yandexcloud.net/codenrock-mts-production-platform-public/competitions/umnaya-polka/umnaya-polka.zip"
ZIP_PATH = Path("/content/umnaya-polka.zip")
OUT_DIR = Path("/content/data")
SEED = 42
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
# Быстрая модель
MODEL_NAME = "resnet18"
# Меньший размер изображения
IMG_SIZE = 160
NUM_CLASSES = 5
# Большой batch для ускорения
BATCH_SIZE = 128
# Только 1 фолд и 1 эпоха
FOLDS_TO_RUN = [0]
EPOCHS = 1
# Берём не весь train, а часть для демонстрации
MAX_TRAIN_ROWS = 4000
LR = 1e-3
WEIGHT_DECAY = 1e-4
N_FOLDS = 5
print("DEVICE:", DEVICE)
if DEVICE == "cpu":
print("ВНИМАНИЕ: GPU не включён. Обучение будет медленным.")
print("Включи Runtime -> Change runtime type -> T4 GPU и перезапусти ячейку.")
# ----------------------------
# 4. Seed
# ----------------------------
def seed_everything(seed=42):
random.seed(seed)
np.random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
seed_everything(SEED)
# ----------------------------
# 5. Скачивание zip из облака
# ----------------------------
def is_good_zip(path: Path):
if not path.exists():
return False
if path.stat().st_size < 10_000_000:
return False
return zipfile.is_zipfile(path)
def download_with_requests(url, path):
headers = {
"User-Agent": "Mozilla/5.0"
}
with requests.get(url, stream=True, headers=headers, timeout=(10, 120)) as r:
r.raise_for_status()
total = int(r.headers.get("content-length", 0))
with open(path, "wb") as f, tqdm(
total=total,
unit="B",
unit_scale=True,
desc="Downloading zip"
) as pbar:
for chunk in r.iter_content(chunk_size=1024 * 1024):
if chunk:
f.write(chunk)
pbar.update(len(chunk))
OUT_DIR.mkdir(parents=True, exist_ok=True)
if is_good_zip(ZIP_PATH):
print("Zip уже скачан и корректен:", ZIP_PATH)
else:
if ZIP_PATH.exists():
print("Найден битый или неполный zip. Удаляю.")
ZIP_PATH.unlink()
print("Скачиваю архив из облака...")
success = False
try:
subprocess.run(
[
"curl",
"-L",
"--fail",
"--retry", "10",
"--retry-delay", "5",
"-o", str(ZIP_PATH),
DATA_URL
],
check=True
)
success = is_good_zip(ZIP_PATH)
except Exception as e:
print("curl не смог скачать файл:", e)
if not success:
try:
if ZIP_PATH.exists():
ZIP_PATH.unlink()
subprocess.run(
[
"wget",
"--tries=10",
"--timeout=60",
"-O", str(ZIP_PATH),
DATA_URL
],
check=True
)
success = is_good_zip(ZIP_PATH)
except Exception as e:
print("wget не смог скачать файл:", e)
if not success:
print("Пробую скачать через Python requests...")
if ZIP_PATH.exists():
ZIP_PATH.unlink()
download_with_requests(DATA_URL, ZIP_PATH)
success = is_good_zip(ZIP_PATH)
assert success, "Не удалось скачать корректный zip-файл."
print("Размер zip-файла:")
subprocess.run(["ls", "-lh", str(ZIP_PATH)])
# ----------------------------
# 6. Распаковка
# ----------------------------
print("Распаковываю архив...")
if OUT_DIR.exists():
subprocess.run(["rm", "-rf", str(OUT_DIR)])
OUT_DIR.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(ZIP_PATH, "r") as z:
z.extractall(OUT_DIR)
print("Архив распакован в:", OUT_DIR)
print("Содержимое /content/data:")
print(os.listdir(OUT_DIR))
# ----------------------------
# 7. Пути к данным
# ----------------------------
ROOT_DIR = OUT_DIR / "umnaya-polka"
if not ROOT_DIR.exists():
if (OUT_DIR / "images").exists() and (OUT_DIR / "track_a").exists():
ROOT_DIR = OUT_DIR
else:
candidates = list(OUT_DIR.rglob("track_a"))
assert len(candidates) > 0, "Не найдена папка track_a после распаковки."
ROOT_DIR = candidates[0].parent
DATA_DIR = ROOT_DIR / "track_a"
IMG_DIR = ROOT_DIR / "images"
print("ROOT_DIR:", ROOT_DIR)
print("DATA_DIR:", DATA_DIR)
print("IMG_DIR:", IMG_DIR)
assert DATA_DIR.exists(), f"Не найдена папка track_a: {DATA_DIR}"
assert IMG_DIR.exists(), f"Не найдена папка images: {IMG_DIR}"
assert (DATA_DIR / "windows_train_a.csv").exists(), "Не найден windows_train_a.csv"
assert (DATA_DIR / "windows_test_a.csv").exists(), "Не найден windows_test_a.csv"
print("Файлы в track_a:")
print(os.listdir(DATA_DIR))
print("Примеры файлов в images:")
print(os.listdir(IMG_DIR)[:10])
# ----------------------------
# 8. Чтение CSV
# ----------------------------
train_df = pd.read_csv(DATA_DIR / "windows_train_a.csv")
test_df = pd.read_csv(DATA_DIR / "windows_test_a.csv")
train_df.columns = train_df.columns.str.strip()
test_df.columns = test_df.columns.str.strip()
print("Исходный train shape:", train_df.shape)
print("test shape:", test_df.shape)
print("Исходное распределение классов:")
print(train_df["class"].value_counts().sort_index())
# ----------------------------
# 9. Уменьшаем train для быстрого обучения
# ----------------------------
sample_parts = []
per_class = MAX_TRAIN_ROWS // NUM_CLASSES
for cls in sorted(train_df["class"].unique()):
cls_df = train_df[train_df["class"] == cls]
take_n = min(len(cls_df), per_class)
sample_parts.append(
cls_df.sample(
n=take_n,
random_state=SEED
)
)
train_df = pd.concat(sample_parts, axis=0).sample(
frac=1,
random_state=SEED
).reset_index(drop=True)
print("Быстрый train shape:", train_df.shape)
print("Распределение классов после сэмплирования:")
print(train_df["class"].value_counts().sort_index())
# ----------------------------
# 10. Поиск путей к изображениям
# ----------------------------
image_files = []
for ext in ["*.jpg", "*.jpeg", "*.png", "*.webp"]:
image_files.extend(list(IMG_DIR.rglob(ext)))
print("Найдено изображений:", len(image_files))
assert len(image_files) > 0, "В папке images не найдены изображения."
by_name = {p.name: p for p in image_files}
by_stem = {p.stem: p for p in image_files}
def resolve_image_path(image_id):
s = str(image_id)
if s in by_name:
return str(by_name[s])
if s in by_stem:
return str(by_stem[s])
for ext in [".jpg", ".jpeg", ".png", ".webp"]:
if s + ext in by_name:
return str(by_name[s + ext])
return None
train_df["image_path"] = train_df["image_id"].apply(resolve_image_path)
test_df["image_path"] = test_df["image_id"].apply(resolve_image_path)
print("Пропуски картинок train:", train_df["image_path"].isna().sum())
print("Пропуски картинок test:", test_df["image_path"].isna().sum())
if train_df["image_path"].isna().sum() > 0:
print("Примеры train image_id без картинки:")
print(train_df.loc[train_df["image_path"].isna(), "image_id"].head(20).tolist())
if test_df["image_path"].isna().sum() > 0:
print("Примеры test image_id без картинки:")
print(test_df.loc[test_df["image_path"].isna(), "image_id"].head(20).tolist())
assert train_df["image_path"].notna().all(), "Есть train-строки без картинки."
assert test_df["image_path"].notna().all(), "Есть test-строки без картинки."
# ----------------------------
# 11. Фолды
# ----------------------------
train_df["fold"] = -1
sgkf = StratifiedGroupKFold(
n_splits=N_FOLDS,
shuffle=True,
random_state=SEED
)
for fold, (tr_idx, val_idx) in enumerate(
sgkf.split(train_df, train_df["class"], groups=train_df["image_id"])
):
train_df.loc[val_idx, "fold"] = fold
print("Фолды:")
print(train_df["fold"].value_counts().sort_index())
# ----------------------------
# 12. Быстрая предобработка без albumentations
# ----------------------------
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
def letterbox_resize(img, size=160):
h, w = img.shape[:2]
if h == 0 or w == 0:
img = np.zeros((size, size, 3), dtype=np.uint8)
return img
scale = size / max(h, w)
new_w = max(1, int(round(w * scale)))
new_h = max(1, int(round(h * scale)))
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
canvas = np.zeros((size, size, 3), dtype=np.uint8)
y0 = (size - new_h) // 2
x0 = (size - new_w) // 2
canvas[y0:y0 + new_h, x0:x0 + new_w] = resized
return canvas
def preprocess_image(img, train=True):
img = letterbox_resize(img, IMG_SIZE)
if train:
if random.random() < 0.5:
img = cv2.flip(img, 1)
if random.random() < 0.25:
alpha = random.uniform(0.85, 1.15)
beta = random.uniform(-10, 10)
img = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
img = img.astype(np.float32) / 255.0
img = (img - MEAN) / STD
img = np.transpose(img, (2, 0, 1))
img = torch.tensor(img, dtype=torch.float32)
return img
# ----------------------------
# 13. Dataset
# ----------------------------
class ShelfDataset(Dataset):
def __init__(self, df, is_test=False, train_mode=False, pad=0.04):
self.df = df.reset_index(drop=True)
self.is_test = is_test
self.train_mode = train_mode
self.pad = pad
def __len__(self):
return len(self.df)
def crop_window(self, row):
img = cv2.imread(row["image_path"])
if img is None:
raise RuntimeError(f"Не удалось прочитать изображение: {row['image_path']}")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
H, W = img.shape[:2]
x = float(row["x"])
y = float(row["y"])
w = float(row["w"])
h = float(row["h"])
px = w * self.pad
py = h * self.pad
x1 = max(0, int(round(x - px)))
y1 = max(0, int(round(y - py)))
x2 = min(W, int(round(x + w + px)))
y2 = min(H, int(round(y + h + py)))
crop = img[y1:y2, x1:x2]
if crop.size == 0:
crop = img
return crop
def __getitem__(self, idx):
row = self.df.iloc[idx]
img = self.crop_window(row)
img = preprocess_image(img, train=self.train_mode)
if self.is_test:
return img
label = int(row["class"])
return img, torch.tensor(label, dtype=torch.long)
# ----------------------------
# 14. Модель ResNet18 с заморозкой backbone
# ----------------------------
def make_model():
model = timm.create_model(
MODEL_NAME,
pretrained=True,
num_classes=NUM_CLASSES
)
# Замораживаем все параметры
for name, param in model.named_parameters():
param.requires_grad = False
# Размораживаем только последний классификатор
for name, param in model.named_parameters():
if (
"fc" in name
or "classifier" in name
or "head" in name
):
param.requires_grad = True
return model
def accuracy_from_logits(logits, targets):
preds = logits.argmax(dim=1)
return (preds == targets).float().mean().item()
# ----------------------------
# 15. Обучение одного фолда
# ----------------------------
def train_one_fold(fold):
print(f"\n========== FOLD {fold} ==========")
tr_df = train_df[train_df["fold"] != fold].copy()
va_df = train_df[train_df["fold"] == fold].copy()
train_ds = ShelfDataset(
tr_df,
is_test=False,
train_mode=True,
pad=0.04
)
valid_ds = ShelfDataset(
va_df,
is_test=False,
train_mode=False,
pad=0.04
)
train_loader = DataLoader(
train_ds,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=2,
pin_memory=(DEVICE == "cuda"),
drop_last=False
)
valid_loader = DataLoader(
valid_ds,
batch_size=BATCH_SIZE * 2,
shuffle=False,
num_workers=2,
pin_memory=(DEVICE == "cuda")
)
model = make_model().to(DEVICE)
trainable_params = [p for p in model.parameters() if p.requires_grad]
print("Обучаемых параметров:", sum(p.numel() for p in trainable_params))
criterion = nn.CrossEntropyLoss(label_smoothing=0.05)
optimizer = torch.optim.AdamW(
trainable_params,
lr=LR,
weight_decay=WEIGHT_DECAY
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=max(1, EPOCHS * len(train_loader)),
eta_min=1e-5
)
scaler = torch.cuda.amp.GradScaler(enabled=(DEVICE == "cuda"))
best_acc = 0.0
best_path = f"/content/best_fast_fold_{fold}.pth"
for epoch in range(1, EPOCHS + 1):
model.train()
train_loss = 0.0
train_acc = 0.0
pbar = tqdm(
train_loader,
desc=f"Fold {fold} | Epoch {epoch}/{EPOCHS} | train"
)
for imgs, labels in pbar:
imgs = imgs.to(DEVICE, non_blocking=True)
labels = labels.to(DEVICE, non_blocking=True)
optimizer.zero_grad(set_to_none=True)
with torch.cuda.amp.autocast(enabled=(DEVICE == "cuda")):
logits = model(imgs)
loss = criterion(logits, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
scheduler.step()
acc = accuracy_from_logits(logits.detach(), labels)
train_loss += loss.item()
train_acc += acc
pbar.set_postfix({
"loss": f"{loss.item():.4f}",
"acc": f"{acc:.4f}"
})
train_loss /= len(train_loader)
train_acc /= len(train_loader)
model.eval()
valid_loss = 0.0
valid_acc = 0.0
with torch.no_grad():
for imgs, labels in tqdm(valid_loader, desc=f"Fold {fold} | valid"):
imgs = imgs.to(DEVICE, non_blocking=True)
labels = labels.to(DEVICE, non_blocking=True)
with torch.cuda.amp.autocast(enabled=(DEVICE == "cuda")):
logits = model(imgs)
loss = criterion(logits, labels)
acc = accuracy_from_logits(logits, labels)
valid_loss += loss.item()
valid_acc += acc
valid_loss /= len(valid_loader)
valid_acc /= len(valid_loader)
print(
f"Epoch {epoch}: "
f"train_loss={train_loss:.4f}, "
f"train_acc={train_acc:.4f}, "
f"valid_loss={valid_loss:.4f}, "
f"valid_acc={valid_acc:.4f}"
)
if valid_acc > best_acc:
best_acc = valid_acc
torch.save(model.state_dict(), best_path)
print(f"Сохранена лучшая модель: {best_path}, acc={best_acc:.5f}")
return best_path, best_acc
# ----------------------------
# 16. Запуск обучения
# ----------------------------
best_models = []
for fold in FOLDS_TO_RUN:
path, acc = train_one_fold(fold)
best_models.append(path)
print("\nГотовые модели:")
print(best_models)
# ----------------------------
# 17. Предсказание на test
# ----------------------------
def predict_with_model(model_path, pad=0.04):
test_ds = ShelfDataset(
test_df,
is_test=True,
train_mode=False,
pad=pad
)
test_loader = DataLoader(
test_ds,
batch_size=BATCH_SIZE * 2,
shuffle=False,
num_workers=2,
pin_memory=(DEVICE == "cuda")
)
model = make_model().to(DEVICE)
model.load_state_dict(torch.load(model_path, map_location=DEVICE))
model.eval()
all_probs = []
with torch.no_grad():
for imgs in tqdm(test_loader, desc=f"Predict {model_path}"):
imgs = imgs.to(DEVICE, non_blocking=True)
with torch.cuda.amp.autocast(enabled=(DEVICE == "cuda")):
logits = model(imgs)
probs = torch.softmax(logits, dim=1)
all_probs.append(probs.cpu().numpy())
return np.concatenate(all_probs, axis=0)
all_predictions = []
for model_path in best_models:
all_predictions.append(
predict_with_model(model_path, pad=0.04)
)
mean_probs = np.mean(all_predictions, axis=0)
test_pred = mean_probs.argmax(axis=1)
print("\nРаспределение предсказанных классов:")
print(pd.Series(test_pred).value_counts().sort_index())
# ----------------------------
# 18. Создание submission
# ----------------------------
sample_path = DATA_DIR / "sample_submission_a.csv"
if sample_path.exists():
sub = pd.read_csv(sample_path)
else:
sub = test_df[["window_id"]].copy()
sub["class"] = 0
sub["class"] = test_pred.astype(int)
SUB_PATH = "/content/pred_a_fast.csv"
sub.to_csv(SUB_PATH, index=False)
print("\nПервые строки submission:")
print(sub.head())
print("\nФайл сохранён:", SUB_PATH)
# ----------------------------
# 19. Проверка формата submission
# ----------------------------
validate_script = DATA_DIR / "validate_submission.py"
if validate_script.exists():
print("\nПроверяю формат submission...")
subprocess.run(
[
sys.executable,
str(validate_script),
"--track", "a",
"--pred", SUB_PATH
],
cwd=str(ROOT_DIR),
check=False
)
else:
print("validate_submission.py не найден, пропускаю проверку.")
# ----------------------------
# 20. Скачивание файла
# ----------------------------
files.download(SUB_PATH)