- BrainTools - https://www.braintools.ru -
Важное предупреждение:
Весь код по моей методологии написан нейросетью DeepSeek, сам бы не осилил (я не программист).
За мной лишь отладка, исправление неточностей и ошибок.
# путь условный, замените на свой
mkdir -p ~/work/..../rubert-tiny2-distill
cd ~/work/..../rubert-tiny2-distill
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
# PyTorch CPU
pip install torch --index-url https://download.pytorch.org/whl/cpu
# Основные библиотеки
pip install
transformers
sentence-transformers
datasets
numpy
scikit-learn
tqdm
huggingface_hub
Проверка:
python -c "import torch, transformers, sentence_transformers; print('OK')"
Ожидаемый вывод: OK
Прежде чем строить студента, нужно ответить на два вопроса:
Какую модель взять за базу? cointegrated/rubert-tiny2 и sergeyzh/rubert-tiny-turbo — обе 3 слоя, 312 dim, 29.4M параметров (turbo — это tiny2, дообученная на contrastive; архитектура та же).
Как агрегировать токены в одно предложение? Учитель использует mean pooling (из карточки модели), но у нас своя голова внимания [1]. Надо понять, какой слой даёт наиболее “семантичный” сигнал.
Метрика для сравнения — gap. Определяется как:
gap = mean(cos на похожих парах) - mean(cos на непохожих)
В “непохожих” в формуле смешаны hard и easy negatives, ниже они показаны отдельно, чтобы видеть вклад каждого типа.
Чем больше gap, тем лучше модель различает семантику и тем “чище” её пространство. У идеальной модели gap был бы большим (похожие — близко, разные — далеко).
Тестовый набор — 10 пар, подобранных специально:
4 позитивные (парафразы): “кошка на стуле” – “кот уселся”, “умер от инфаркта” – “жизнь оборвал сердечный приступ”.
3 hard negatives (отрицания): “он устроился” – “он НЕ устроился”, “я люблю кошек” – “я НЕ люблю”, “назначил” – “отменил”.
3 обычных негатива (разные темы): “кошка” – “самолёт”, “борщ” – “Пифагор”.
Перебираем комбинации:
Слои: embeddings, L0, L1, L2 (все выходы модели).
Pooling-стратегии: mean, cls, max, last-token.
Это разведочный набор, а не оценка качества. Эти 10 пар нужны, чтобы выбрать слой и pooling-стратегию. Выводы о качестве делаются отдельно — на hnp_val (590 пар) и внешнем наборе (5000 пар) в следующих разделах.
Цель: найти комбинацию с максимальным gap. Также важно посмотреть на cos hard negatives.
Первый запуск inspect_layers.py скачает ~940 MB (DeepPavlov ~700 MB + turbo ~120 MB + tiny2 ~120 MB).
Файл: scripts/1.1_inspect_layers.py
"""Разведка слоёв и pooling-стратегий.
Сравниваем:
- cointegrated/rubert-tiny2 (базовая LM, 3 слоя, 312 dim)
- sergeyzh/rubert-tiny-turbo (sentence encoder, 312 dim)
- DeepPavlov/rubert-base-cased-sentence (эталон, 12 слоёв, 768 dim)
Pooling: mean / CLS / max / last-token
"""
import sys
from pathlib import Path
import numpy as np
import torch
import torch.nn.functional as F
from transformers import AutoModel, AutoTokenizer
DEVICE = torch.device("cpu")
torch.manual_seed(42); np.random.seed(42)
MODELS = [
("cointegrated/rubert-tiny2", "base LM"),
("sergeyzh/rubert-tiny-turbo", "sentence enc"),
("DeepPavlov/rubert-base-cased-sentence", "teacher"),
]
PAIRS = [
("кошка сидит на стуле", "кот уселся на стуле", 1),
("он умер от инфаркта", "его жизнь оборвал сердечный приступ", 1),
("психология травмы и стадии горя", "этапы переживания утраты", 1),
("студент сдал экзамен на отлично", "учащийся успешно прошёл испытание", 1),
("он устроился на работу", "он не устроился на работу", 0), # hard
("я люблю кошек", "я не люблю кошек", 0), # hard
("врач назначил лечение", "врач отменил лечение", 0), # hard
("кошка сидит на стуле", "самолёт летит в небе", 0), # тот же anchor, теперь с негативом
("повар готовит борщ", "теорема Пифагора", 0),
("история Древнего Рима", "строение ДНК", 0),
]
HARD_NEG_INDICES = [4, 5, 6] # индексы hard negatives в PAIRS (0-based)
def mean_pool(h, m):
mm = m.unsqueeze(-1).float()
return (h * mm).sum(1) / mm.sum(1).clamp(min=1e-9)
def cls_pool(h, m):
return h[:, 0]
def max_pool(h, m):
mm = m.unsqueeze(-1).float()
h = h.masked_fill(mm == 0, float("-inf"))
return h.max(1).values
def last_pool(h, m):
# индекс последнего реального токена
lengths = (m.sum(1) - 1).clamp(min=0) # [B]
return h[torch.arange(h.size(0)), lengths]
POOLERS = {
"mean": mean_pool,
"cls": cls_pool,
"max": max_pool,
"last": last_pool,
}
def l2(x):
return F.normalize(x, dim=-1)
def evaluate_model(model_id, tag):
print(f"n{'='*80}")
print(f"Model: {model_id} ({tag})")
print(f"{'='*80}", flush=True)
model = AutoModel.from_pretrained(model_id).to(DEVICE).eval()
tokenizer = AutoTokenizer.from_pretrained(model_id)
cfg = model.config
print(f" layers={cfg.num_hidden_layers} hidden={cfg.hidden_size} "
f"heads={cfg.num_attention_heads} vocab={cfg.vocab_size} "
f"max_pos={cfg.max_position_embeddings}")
n_params = sum(p.numel() for p in model.parameters())
print(f" params={n_params:,} ({n_params*4/1e6:.1f} MB fp32)")
texts = [a for a, _, _ in PAIRS] + [b for _, b, _ in PAIRS]
labels = [lbl for _, _, lbl in PAIRS]
n = len(PAIRS)
enc = tokenizer(texts, padding=True, truncation=True, max_length=256,
return_tensors="pt")
with torch.no_grad():
out = model(**enc, output_hidden_states=True)
hidden_states = out.hidden_states # tuple (n_layers+1)
print(f"n {'layer':<12} {'pool':<6} {'sim+':>7} {'sim-':>7} {'gap':>7} {'hard-':>7}")
print(f" {'-'*50}")
best = (None, -999)
for layer_idx, hidden in enumerate(hidden_states):
for pool_name, pool_fn in POOLERS.items():
emb = l2(pool_fn(hidden, enc["attention_mask"]))
pos_s, neg_s, hard_s = [], [], []
for i, (a, b, lbl) in enumerate(PAIRS):
s = float((emb[i] * emb[n + i]).sum())
if i in HARD_NEG_INDICES:
hard_s.append(s)
if lbl == 1:
pos_s.append(s)
else:
neg_s.append(s)
p, nn, hh = np.mean(pos_s), np.mean(neg_s), np.mean(hard_s)
gap = p - nn
layer_name = f"emb" if layer_idx == 0 else f"L{layer_idx-1}"
print(f" {layer_name:<12} {pool_name:<6} {p:>7.3f} {nn:>7.3f} "
f"{gap:>7.3f} {hh:>7.3f}")
if gap > best[1]:
best = ((layer_name, pool_name, emb), gap)
# Разбор лучшего
(layer_name, pool_name, emb), gap = best
print(f"n Лучшая комбинация: layer={layer_name} pool={pool_name} gap={gap:.3f}")
print(f" Разбор пар:")
for i, (a, b, lbl) in enumerate(PAIRS):
s = float((emb[i] * emb[n + i]).sum())
tag = "POS" if lbl == 1 else ("HRD" if i in HARD_NEG_INDICES else "NEG")
print(f" [{tag}] {s:+.3f} {a[:40]:<42} <-> {b[:40]}")
return best
def main():
print(f"Device: {DEVICE}")
results = {}
for model_id, tag in MODELS:
try:
best, gap = evaluate_model(model_id, tag)
results[model_id] = (best[0], best[1], gap)
except Exception as e:
print(f"n[!] {model_id} failed: {e}")
results[model_id] = None
print(f"n{'='*80}")
print("ИТОГ")
print(f"{'='*80}")
for model_id, r in results.items():
if r is None:
print(f" {model_id:<50} FAILED")
else:
layer, pool, gap = r
print(f" {model_id:<50} best={layer}/{pool} gap={gap:.3f}")
if __name__ == "__main__":
main()
Запуск:
cd ~/work/..../rubert-tiny2-distill
source .venv/bin/activate
python scripts/inspect_layers.py
Время: ~2 минуты (три модели, минимальный тест).
|
Модель |
Лучшая комбинация |
Gap |
|---|---|---|
|
|
L2 / cls |
0.064 |
|
|
L2 / mean |
0.125 |
|
|
L11 / mean |
0.329 |
Три вывода:
Turbo почти в 2 раза сильнее tiny2. Обе модели имеют одинаковую архитектуру (3 слоя, 312 dim), но turbo дообучен contrastive на русских данных (см. карточку модели). Это дало ему готовое понимание семантики, тогда как tiny2 остаётся базовой MLM-моделью, её представления не оптимизированы под сравнение текстов.
Учитель — в 2.5x сильнее turbo. DeepPavlov с 12 слоями и 768 dim имеет гораздо более богатое представление. Наша задача — не догнать его по абсолютной семантике, а приблизиться настолько, чтобы retrieval и дедубликация работали.
Учитель страдает от negation blindness (см. 1.5).
Это важное открытие инспекции. Смотрим, что даёт DeepPavlov на hard negatives (лучшая комбинация — L11/mean):
|
Пара |
cos |
|---|---|
|
“он устроился на работу” – “он не устроился на работу” |
0.578 |
|
“я люблю кошек” – “я не люблю кошек” |
0.702 |
|
“врач назначил лечение” – “врач отменил лечение” |
0.655 |
Модель даёт высокое сходство (0.58–0.70) на парах, где семантика противоположна.
Причина — в архитектуре и pooling. Базовая rubert-base-cased — MLM-модель: она учится восстанавливать токены по контексту, и вклад одного токена (“не”) размывается. Sentence-версия дообучена на парах, но использует mean pooling, который усредняет все токены и ещё сильнее ослабляет сигнал отрицания.
Практическое следствие:
Порог дедубликации на таких парах срабатывает ложно.
Семантический поиск ставит “противоположное” на высокие позиции.
Для задач, где отрицания важны (психотерапия, юриспруденция, медицина), учитель работает плохо.
Через contrastive на ru-HNP мы попробуем сбивать cos на hard negatives у ученика, чтобы он не перенес на себя “болезнь” учителя.
Берём sergeyzh/rubert-tiny-turbo. Причины:
Стартовый gap почти в 2 раза выше — семантику не надо учить с нуля — только адаптировать.
Contrastive-предобучение — должен сойтись быстрее в Phase B.
Что забираем у turbo:
Только энкодер (3 слоя, 312 dim).
Его родной pooling (mean) игнорируем — заменим своим Attention Pooling.
Hidden states сохраняют contrastive-обученные представления, это ключевое.
Автор: AntonovPsy
Источник [2]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/37756
URLs in this post:
[1] внимания: http://www.braintools.ru/article/7595
[2] Источник: https://habr.com/ru/articles/1100874/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1100874
Нажмите здесь для печати.