Зачем вообще обучать модель для обфускации?
Представьте: у вас есть тонны клиентских данных на бразильском португальском - чеки, переписки, медицинские записи. Всё это нужно передать аналитикам или в AI-сервисы, но нельзя показывать реальные имена, телефоны, адреса. Ручная замена - это ад. Регулярные выражения ломаются на опечатках. GPT-4 API стоит денег и отправляет данные куда-то в облако.
Решение? Обучить маленькую модель, которая будет жить у вас на сервере и заменять PII (персонально идентифицируемую информацию) на реалистичные, но вымышленные аналоги. Gemma-3 270M - идеальный кандидат: весит около 1.5 ГБ, работает на CPU, а с Unsloth её можно дообучить за пару часов даже на дешёвой видеокарте.
Важно: Gemma-3 270M - самая маленькая модель в семействе на январь 2026 года. Она не заменит GPT-4 в сложных задачах, но для детерминированных преобразований вроде обфускации подходит идеально.
Что пойдёт не так, если делать по учебникам
Большинство гайдов по fine-tuning'у показывают общие принципы, но упускают критичные для обфускации детали:
- Модель начнёт "сочинять" - вместо замены имени "Мария" на "Ана", она может генерировать целые предложения
- Контекст съест память - если подавать длинные тексты, 270M параметров не хватит
- Формат данных сломает обучение - неправильный JSONL = потраченные часы тренировки впустую
- Обфускация станет обратимой - если замена предсказуема, можно восстановить оригинал
Подготовка: что нужно перед стартом
1 Железо и софт
| Компонент | Минимум | Рекомендуется |
|---|---|---|
| GPU VRAM | 8 ГБ (RTX 3070) | 16 ГБ (RTX 4080) |
| RAM | 16 ГБ | 32 ГБ |
| Диск | 10 ГБ свободно | 20 ГБ свободно |
| Python | 3.10 | 3.11+ |
# Установка всего необходимого
pip install "unsloth[cu121] @ git+https://github.com/unslothai/unsloth.git"
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers datasets accelerate trl peft
2 Создание датасета
Вот где большинство спотыкаются. Нужно не просто собрать тексты, а создать парные примеры: оригинал → обфусцированная версия. Для бразильского португальского я использовал 1700 примеров такого формата:
{
"instruction": "Substitua todas as informações pessoais (PII) por dados fictícios realistas, mantendo a estrutura do texto.",
"input": "O paciente João Silva, CPF 123.456.789-00, residente na Rua das Flores, 123, São Paulo-SP, telefone (11) 99999-9999, foi atendido com queixa de dor abdominal.",
"output": "O paciente Lucas Oliveira, CPF 987.654.321-00, residente na Avenida Paulista, 456, São Paulo-SP, telefone (11) 98888-8888, foi atendido com queixa de dor abdominal."
}
Ключевые моменты:
- Одинаковая длина - input и output должны быть примерно одинаковой длины
- Сохранение форматов - CPF остаётся в формате XXX.XXX.XXX-XX, телефон в формате (XX) XXXXX-XXXX
- Реалистичность замен - "João Silva" → "Lucas Oliveira" (оба бразильские имена), а не "John Smith"
- Контекстуальная замена - адрес в Сан-Паулу заменяется на другой адрес в Сан-Паулу
Не делайте так: Создавать датасет, где модель просто удаляет PII. Это проще, но тогда модель научится удалять куски текста, что ломает структуру документов.
Код обучения: от загрузки модели до инференса
3 Загрузка модели и токенизатора
from unsloth import FastLanguageModel
import torch
from transformers import TrainingArguments
from trl import SFTTrainer
from datasets import load_dataset
# Загружаем Gemma-3 270M через Unsloth
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="google/gemma-3-270m",
max_seq_length=2048, # Увеличиваем для длинных документов
dtype=torch.float16, # Экономия памяти
load_in_4bit=True, # QLoRA 4-bit
device_map="auto",
)
# Добавляем адаптеры LoRA
model = FastLanguageModel.get_peft_model(
model,
r=16, # Ранг LoRA
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0.1,
bias="none",
use_gradient_checkpointing=True,
random_state=42,
max_seq_length=2048,
)
4 Подготовка датасета
# Загружаем наш JSONL датасет
dataset = load_dataset("json", data_files="pii_obfuscation_ptbr.jsonl", split="train")
# Форматируем промпты
def format_instruction(example):
text = f"""Abaixo está uma instrução que descreve uma tarefa, juntamente com uma entrada que fornece mais contexto. Escreva uma resposta que complete adequadamente a solicitação.
### Instrução:
{example['instruction']}
### Entrada:
{example['input']}
### Resposta:
{example['output']}"""
return {"text": text}
dataset = dataset.map(format_instruction)
# Токенизация
def tokenize_function(examples):
return tokenizer(
examples["text"],
truncation=True,
padding="max_length",
max_length=2048,
)
tokenized_dataset = dataset.map(tokenize_function, batched=True)
Ошибка, которую делают 90% людей: они токенизируют input и output отдельно. Так модель не учится связи между ними. Надо токенизировать весь промпт целиком.
5 Конфигурация обучения
training_args = TrainingArguments(
output_dir="./gemma3-270m-pii-obfuscator",
num_train_epochs=3, # 3 эпохи достаточно для 1700 примеров
per_device_train_batch_size=4, # Увеличиваем если VRAM позволяет
gradient_accumulation_steps=4, # Эффективный batch size = 16
warmup_steps=50,
logging_steps=10,
save_steps=500,
eval_steps=500,
evaluation_strategy="steps",
save_total_limit=3,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
fp16=True, # Обязательно для экономии памяти
learning_rate=2e-4, # Gemma-3 любит высокий LR
optim="adamw_8bit", # 8-bit AdamW из bitsandbytes
report_to="none", # Не отправляем в WandB/TensorBoard
)
# Создаём тренера
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
dataset_text_field="text",
max_seq_length=2048,
tokenizer=tokenizer,
packing=False, # Не пакуем - у нас структурированные промпты
)
Внимание на learning_rate: Для Gemma-3 270M ставлю 2e-4, а не стандартные 5e-5. Маленькие модели требуют более агрессивного обучения, иначе они просто запоминают датасет без обобщения.
6 Запуск обучения и сохранение
# Запускаем обучение
trainer.train()
# Сохраняем адаптеры LoRA
model.save_pretrained("./gemma3-270m-pii-obfuscator-lora")
tokenizer.save_pretrained("./gemma3-270m-pii-obfuscator-lora")
# Сохраняем полную модель для инференса (опционально)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./gemma3-270m-pii-obfuscator-merged",
safe_serialization=True)
Инференс: как использовать обученную модель
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
# Загружаем модель и адаптеры
tokenizer = AutoTokenizer.from_pretrained("./gemma3-270m-pii-obfuscator-lora")
model = AutoModelForCausalLM.from_pretrained(
"google/gemma-3-270m",
torch_dtype=torch.float16,
device_map="auto",
)
model.load_adapter("./gemma3-270m-pii-obfuscator-lora")
# Функция для обфускации
def obfuscate_pii(text, max_length=1024):
prompt = f"""Abaixo está uma instrução que descreve uma tarefa, juntamente com uma entrada que fornece mais contexto. Escreva uma resposta que complete adequadamente a solicitação.
### Instrução:
Substitua todas as informações pessoais (PII) por dados fictícios realistas, mantendo a estrutura do texto.
### Entrada:
{text}
### Resposta:
"""
inputs = tokenizer(prompt, return_tensors="pt", truncation=True,
max_length=max_length).to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=len(text) + 100, # Немного больше оригинала
temperature=0.1, # Низкая температура для детерминированности
do_sample=False,
pad_token_id=tokenizer.eos_token_id,
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Вырезаем только ответ после "### Resposta:"
resposta_start = response.find("### Resposta:") + len("### Resposta:")
return response[resposta_start:].strip()
# Пример использования
texto_original = "Cliente: Maria Santos, email: maria.santos@email.com, telefone: (21) 98765-4321"
texto_obfuscado = obfuscate_pii(texto_original)
print(f"Original: {texto_original}")
print(f"Obfuscado: {texto_obfuscado}")
Типичные проблемы и их решения
| Проблема | Причина | Решение |
|---|---|---|
| Модель генерирует мусор | Слишком высокая температура | temperature=0.1 или do_sample=False |
| Out of memory ошибка | Слишком большой max_length | Уменьшить до 1024 или использовать gradient checkpointing |
| Модель повторяет input | Переобучение на датасете | Уменьшить эпохи до 2, добавить dropout |
| Не заменяет все PII | Недостаточно разнообразных примеров | Добавить больше вариаций в датасет |
| Медленный инференс | Модель на CPU | Использовать квантование GGUF для CPU |
Что дальше? Улучшения и оптимизации
Обученная модель работает, но можно лучше:
- Добавление типов PII - научить модель различать типы данных: CPF → CPF, email → email, адрес → адрес
- Контекстуальная замена - если в тексте "доктор Карлос" и "пациент Карлос", заменять их на разные имена
- Мультиязычность - дообучить на английских и испанских примерах
- Специализация - отдельные модели для медицинских записей, финансовых документов, переписок
Если нужно ускорить инференс в 2-3 раза, смотрите гайд по квантованию Gemma-3. Для сложных задач, где нужна логика поверх обфускации, может помочь настройка вызова процедур.
Сколько это стоит и стоит ли вообще?
Давайте считать:
- Электричество - 3 часа обучения на RTX 4070 ≈ 0.5 кВт·ч ≈ 5 рублей
- Время - подготовка датасета (8 часов) + обучение (3 часа) + тестирование (2 часа)
- Альтернатива - GPT-4 API: 1700 примеров × 500 токенов × 0.03$/1K токенов ≈ 25$ за один прогон
Выгода появляется после 1000 документов. Плюс:
- Данные не уходят в облако
- Нет лимитов на запросы
- Можно дообучать под специфику ваших данных
- Работает оффлайн
Главный подводный камень? Модель на 270M параметров иногда "забывает" редкие форматы данных. Если у вас встречаются нестандартные номера паспортов или специализированные идентификаторы - добавьте их в датасет с запасом. Лучше 50 примеров редкого формата, чем надеяться, что модель догадается.
И последнее: не экономьте на валидационной выборке. Отложите 200 примеров из 1700 и не смотрите на них во время обучения. Только так поймёте, действительно ли модель обобщает, а не заучивает.