- **Платформа:** Windows 10/11 + WSL2 (Ubuntu 22.04 / 24.04) - **Железо:** NVIDIA RTX 3060 Ti (8 ГБ VRAM) - **Базовая модель для обучения:** Meta Llama 3.1 8B (`unsloth/llama-3.1-8b-Instruct-bnb-4bit`) - **Рабочая директория:** `C:\ST\aandc\` (внутри WSL2: `/mnt/c/ST/aandc/`) --- ЧАСТЬ 1. Развертывание среды и проброс GPU в WSL2 Шаг 1.1. Базовая установка подсистемы Откройте **PowerShell от имени Администратора** на хостовой Windows: powershell ``` wsl --install wsl --set-default-version 2 ``` Используйте код с осторожностью. _Перезагрузите компьютер. При первом запуске терминала Ubuntu задайте логин и пароль._ Шаг 1.2. Защита от Out Of Memory (Выделение ресурсов) 1. В Windows нажмите `Win + R`, введите `%USERPROFILE%` и нажмите Enter. 2. Создайте в этой папке текстовый файл `.wslconfig` (без расширения `.txt`) и вставьте: ini ``` [wsl2] memory=16GB # Выделяем минимум 16 ГБ оперативной памяти (или 75% от системной RAM) swap=8GB # Обязательный файл подкачки для подстраховки при компиляции моделей localhostForwarding=true ``` Используйте код с осторожностью. 3. Перезапустите WSL в PowerShell: `wsl --shutdown` Шаг 1.3. Настройка NVIDIA Container Toolkit Запустите терминал **Ubuntu в WSL2** и выполните цепочку команд для чистой настройки проброса видеокарты: bash ``` sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg2 # Удаляем старые битые кэши и ключи, если они создались sudo rm -f /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo rm -f /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg # Скачивание официального валидного GPG-ключа NVIDIA curl -fsSL https://github.io | sudo gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg # Добавление стабильного репозитория пакетов curl -s -L https://github.io | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list # Обновление списков и установка утилиты сопряжения sudo apt-get update sudo apt-get install -y nvidia-container-toolkit ``` Используйте код с осторожностью. _Для проверки введите `nvidia-smi`. На экране должна появиться таблица вашей видеокарты RTX 3060 Ti._ --- ЧАСТЬ 2. Установка Ollama и Запуск Базовой Модели Шаг 2.1. Установка движка и скачивание Llama 3.1 Выполните в терминале WSL2: bash ``` curl -fsSL https://ollama.com | sh ollama pull llama3.1 ``` Используйте код с осторожностью. Шаг 2.2. Фоновый запуск сервера Ollama Поскольку менеджер служб `systemd` в WSL часто ограничен, запускайте Ollama напрямую встроенным фоновым процессом Linux: bash ``` ollama serve > /dev/null 2>&1 & ``` Используйте код с осторожностью. _(Нажмите Enter, если терминал временно застынет. Проверить статус можно командой `ollama list`)._ --- ЧАСТЬ 3. Автоматизированная генерация ShareGPT-датасета Шаг 3.1. Подготовка файлов в Windows и WSL2 1. В Windows создайте папку `C:\ST\aandc\manuals\`. 2. Скопируйте в неё все ваши **71 исходный технический PDF-мануал**. Внутри WSL2 они мгновенно станут доступны по пути `/mnt/c/ST/aandc/manuals/`. 3. В терминале WSL2 установите библиотеки обработки данных (обходя ограничения `PEP 668` системы Ubuntu): bash ``` pip3 install pypdf requests --break-system-packages ``` Используйте код с осторожностью. Шаг 3.2. Развертывание отказоустойчивого скрипта `generate_qa.py` Создайте файл `/mnt/c/ST/aandc/generate_qa.py` и скопируйте в него этот код. Он очищает текст регулярными выражениями и корректно парсит ответы Ollama, даже если она возвращает одиночный словарь вместо списка: python ``` import os import re import json import pypdf import requests # === КОНФИГУРАЦИЯ === MODEL_NAME = "llama3.1:latest" OLLAMA_URL = "http://localhost:11434/api/generate" MANUALS_DIR = "/mnt/c/ST/aandc/manuals" OUTPUT_FILE = "/mnt/c/ST/aandc/aandc_dataset.jsonl" WINDOW_SIZE = 3 # Размер окна чтения документа (в страницах) SYSTEM_PROMPT = ( "Вы — опытный technical writer и эксперт по банкоматному ПО.\n" "Изучите предоставленный фрагмент технической документации.\n" "Создайте от 2 до 4 высококачественных пар 'Вопрос-Ответ' на основе ТОЛЬКО этого текста.\n" "Вопросы должны быть конкретными (коды ошибок, процедуры калибровки, настройка систем).\n" "Ответы должны быть глубокими, технически точными, структурированными и без упоминания номеров страниц.\n" "Формат ответа строго JSON списка ShareGPT:\n" '[{"conversations": [{"from": "human", "value": "Вопрос"}, {"from": "gpt", "value": "Ответ"}]}]' ) def clean_text(text): """Очистка текста от мусора, номеров страниц и колонтитулов""" text = re.sub(r'Page \d+ of \d+', '', text, flags=re.IGNORECASE) text = re.sub(r'^\d+\s*$', '', text, flags=re.MULTILINE) text = re.sub(r'\n+', '\n', text) text = re.sub(r' +', ' ', text) return text.strip() def extract_pdf_chunks(pdf_path, window_size=3): """Динамическое чтение любого PDF файла порциями страниц""" try: with open(pdf_path, 'rb') as f: reader = pypdf.PdfReader(f) total_pages = len(reader.pages) for i in range(0, total_pages, window_size): chunk_text = "" for j in range(i, min(i + window_size, total_pages)): page_text = reader.pages[j].extract_text() if page_text: chunk_text += page_text + "\n" cleaned = clean_text(chunk_text) if len(cleaned) > 100: yield cleaned except Exception as e: print(f"Ошибка при чтении файла {os.path.basename(pdf_path)}: {e}") def ask_ollama(context): """Запрос к локальному серверу Ollama за строгим JSON-форматом""" prompt = f"ДОКУМЕНТАЦИЯ:\n{context}\n\nСгенерируй JSON по инструкции." payload = { "model": MODEL_NAME, "prompt": prompt, "system": SYSTEM_PROMPT, "stream": False, "format": "json" } try: response = requests.post(OLLAMA_URL, json=payload) if response.status_code == 200: return response.json().get("response", "") except Exception as e: print(f"Ошибка связи с Ollama: {e}") return None def main(): if not os.path.exists(MANUALS_DIR): print(f"Ошибка: Папка {MANUALS_DIR} не найдена!") return pdf_files = [f for f in os.listdir(MANUALS_DIR) if f.lower().endswith('.pdf')] total_files = len(pdf_files) if total_files == 0: print(f"В папке {MANUALS_DIR} не найдено ни одного PDF файла.") return print(f"Найдено файлов для обработки: {total_files}") print("Старт генерации профессионального датасета...") # Файл затирает старую сырую базу 'w' и наполняется построчно (JSON Lines) with open(OUTPUT_FILE, 'w', encoding='utf-8') as outfile: for idx, pdf_name in enumerate(pdf_files, 1): pdf_path = os.path.join(MANUALS_DIR, pdf_name) print(f"[{idx}/{total_files}] Обработка: {pdf_name}") for chunk in extract_pdf_chunks(pdf_path, WINDOW_SIZE): raw_json = ask_ollama(chunk) if not raw_json: continue try: data = json.loads(raw_json) # Сценарий 1: Модель вернула список объектов [ {...}, {...} ] if isinstance(data, list): for item in data: if isinstance(item, dict) and "conversations" in item: outfile.write(json.dumps(item, ensure_ascii=False) + "\n") outfile.flush() print(" ✅ Записана карточка (из списка)") # Сценарий 2: Модель вернула один одиночный объект { "conversations": [...] } elif isinstance(data, dict): if "conversations" in data: outfile.write(json.dumps(data, ensure_ascii=False) + "\n") outfile.flush() print(" ✅ Записана карточка (прямой словарь)") else: for key, val in data.items(): if isinstance(val, list): for sub_item in val: if isinstance(sub_item, dict) and "conversations" in sub_item: outfile.write(json.dumps(sub_item, ensure_ascii=False) + "\n") outfile.flush() print(" ✅ Записана карточка (из вложенного ключа)") except json.JSONDecodeError: continue print(f"\nУспешно! Все файлы обработаны. Новый датасет сохранен в: {OUTPUT_FILE}") if __name__ == "__main__": main() ``` Используйте код с осторожностью. _Запуск: `python3 /mnt/c/ST/aandc/generate_qa.py`. На выходе сформируется файл на 756 качественных диалогов._ --- ЧАСТЬ 4. Стабильный Fine-Tuning LoRA на 8 ГБ VRAM Для того чтобы обучение гарантированно шло без вылетов по памяти (`RuntimeError по fused cross entropy`), мы используем чистый, промышленный стек **Hugging Face (`transformers` + `peft` + `bitsandbytes`)**. Мы полностью убрали капризные компиляторы Unsloth, исключив любые пики выделения памяти на карте хоста. Шаг 4.1. Установка стабильных зависимостей Выполните в терминале WSL2: bash ``` pip install torch torchvision torchaudio --index-url https://pytorch.org --break-system-packages pip install transformers datasets peft bitsandbytes --break-system-packages ``` Используйте код с осторожностью. Шаг 4.2. Развертывание «железного» скрипта обучения `train.py` Создайте файл `/mnt/c/ST/aandc/train.py` и скопируйте в него этот код: python ``` import os import json import torch from datasets import Dataset from transformers import ( AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments, Trainer, DataCollatorForLanguageModeling ) from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training # === КОНФИГУРАЦИЯ ДЛЯ ГАРАНТИРОВАННОГО СТАРТА НА 8 ГБ VRAM === MODEL_NAME = "unsloth/llama-3.1-8b-Instruct-bnb-4bit" # Весовая база max_seq_length = 512 # Идеальный размер контекста для защиты памяти от пиков DATASET_PATH = "/mnt/c/ST/aandc/aandc_dataset.jsonl" # Принудительная очистка видеопамяти torch.cuda.empty_cache() print("--- 1. Конфигурация 4-битного квантования bitsandbytes ---") bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True ) print("--- 2. Загрузка модели и токенизатора напрямую через HF ---") tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, quantization_config=bnb_config, device_map="auto" ) # Подготовка модели к PEFT обучению (стабилизация градиентов k-bit) model = prepare_model_for_kbit_training(model) print("--- 3. Конфигурация LoRA (Чистый PEFT) ---") peft_config = LoraConfig( r=16, lora_alpha=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, peft_config) print("--- 4. Отказоустойчивая валидация и загрузка датасета ---") valid_dialogues = [] with open(DATASET_PATH, "r", encoding="utf-8") as f: for idx, line in enumerate(f, 1): if not line.strip(): continue try: item = json.loads(line) if "conversations" not in item or not isinstance(item["conversations"], list): continue clean_conv = [] is_corrupted = False for msg in item["conversations"]: # Защита от изменения типов (строка вместо словаря на row 9) if not isinstance(msg, dict) or "from" not in msg or "value" not in msg: is_corrupted = True break clean_conv.append({ "from": str(msg["from"]).strip(), "value": str(msg["value"]).strip() }) if not is_corrupted and len(clean_conv) > 0: valid_dialogues.append({"conversations": clean_conv}) except json.JSONDecodeError: continue print(f"Успешно загружено чистых диалогов: {len(valid_dialogues)}") dataset = Dataset.from_list(valid_dialogues) # Форматирование и ЯВНАЯ ТОКЕНИЗАЦИЯ строк под формат ролей Llama 3.1 def tokenize_prompts_func(examples): conversations = examples["conversations"] tokenized_inputs = {"input_ids": [], "attention_mask": []} for con in conversations: mapped_conversation = [] for msg in con: # Превращаем "human" в "user", "gpt" в "assistant" для шаблона Llama 3.1 role = "user" if msg["from"] == "human" else "assistant" mapped_conversation.append({"role": role, "content": msg["value"]}) # Применяем Jinja-шаблон чата text = tokenizer.apply_chat_template(mapped_conversation, tokenize=False, add_generation_prompt=False) # Токенизируем текст напрямую с жесткой обрезкой до max_seq_length tokenized = tokenizer(text, truncation=True, max_length=max_seq_length, padding=False) tokenized_inputs["input_ids"].append(tokenized["input_ids"]) tokenized_inputs["attention_mask"].append(tokenized["attention_mask"]) return tokenized_inputs # Пересобираем датасет в готовые числовые тензоры dataset = dataset.map(tokenize_prompts_func, batched=True, remove_columns=["conversations"]) print("--- 5. Инициализация индустриального Trainer ---") trainer = Trainer( model=model, train_dataset=dataset, data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False), # Сборщик батчей args=TrainingArguments( per_device_train_batch_size=1, # Размер батча 1 исключает переполнение VRAM gradient_accumulation_steps=8, # Накопление шагов (1 * 8 эмулирует реальный батч размера 8) warmup_steps=5, max_steps=60, # Обучение на 60 шагах. Для финала: num_train_epochs = 3 learning_rate=2e-4, fp16=True, logging_steps=1, optim="adamw_8bit", # 8-битный оптимизатор экономит драгоценную VRAM weight_decay=0.01, lr_scheduler_type="linear", seed=3407, output_dir="outputs", remove_unused_columns=False ), ) print("--- 6. СТАРТ ТРЕНИРОВКИ (Чистый стек Hugging Face) ---") trainer.train() print("--- ТРЕНИРОВКА УСПЕШНО ЗАВЕРШЕНА ---") print("--- 7. Сохранение LoRA адаптеров ---") model.save_pretrained("/mnt/c/ST/aandc/aandc_lora_model") tokenizer.save_pretrained("/mnt/c/ST/aandc/aandc_lora_model") print("Обученные адаптеры сохранены в папку aandc_lora_model!") ``` Используйте код с осторожностью. Шаг 4.3. Выполнение тренировки Перед запуском обязательно **выгрузите Ollama из памяти**, чтобы освободить видеокарту хоста: bash ``` pkill -f ollama ``` Используйте код с осторожностью. Запустите скрипт: bash ``` python3 /mnt/c/ST/aandc/train.py ``` Используйте код с осторожностью. _Процесс займет около 2 часов. Значение `loss` будет плавно падать, фиксируя технические знания в весах LoRA._ --- ЧАСТЬ 5. Слияние Модели в Совместимый Монолит Поскольку базовая модель `bnb-4bit` аппаратно заблокирована от обратной распаковки на CPU через `llama.cpp`, мы используем официальный индустриальный метод: **накатим ваши адаптеры на открытую, чистую базовую модель Llama 3.1 8B в формате `float16`** и запишем один полноценный монолитный файл. Шаг 5.1. Запуск скрипта слияния (Merge & Unload) Запустите интерактивный Python в терминале WSL2: bash ``` python3 ``` Используйте код с осторожностью. Вставьте этот код (он скачает чистую базу от Unsloth и объединит её с вашими адаптерами): python ``` import torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base_model_path = "unsloth/llama-3.1-8b-Instruct" lora_path = "/mnt/c/ST/aandc/aandc_lora_model" output_path = "/mnt/c/ST/aandc/atm_expert_fused_clean" print("1. Загрузка открытой базовой модели Llama 3.1 (float16)...") base_model = AutoModelForCausalLM.from_pretrained(base_model_path, torch_dtype=torch.bfloat16, device_map="cpu") tokenizer = AutoTokenizer.from_pretrained(base_model_path) print("2. Наложение обученных LoRA весов...") model = PeftModel.from_pretrained(base_model, lora_path) print("3. Слияние матриц в единый монолит...") model = model.merge_and_unload() print("4. Сохранение финальной совместимой модели...") model.save_pretrained(output_path, safe_serialization=True) tokenizer.save_pretrained(output_path) print("--- СЛИЯНИЕ УСПЕШНО ЗАВЕРШЕНО ---") exit() ``` Используйте код с осторожностью. _После завершения в директории `C:\ST\aandc\atm_expert_fused_clean\` появится полноценная, совместимая с Ollama модель._ --- ЧАСТЬ 6. Сборка и Запуск Финального ИИ-Эксперта Шаг 6.1. Настройка конфигурационного файла `Modelfile` Перепишите ваш `Modelfile`, чтобы Ollama прочитала готовую монолитную папку, выполнив в консоли команду: bash ``` cat << 'EOF' > /mnt/c/ST/aandc/Modelfile # Ссылка на созданную монолитную папку FROM /mnt/c/ST/aandc/atm_expert_fused_clean TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|> {{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|> {{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|> {{ .Response }}<|eot_id|>""" SYSTEM "Вы — специализированный локальный ИИ-эксперт по банкоматному ПО. Вы отвечаете строго на основе технической документации, четко, структурированно, используя точные инженерные термины и коды ошибок." EOF ``` Используйте код с осторожностью. Шаг 6.2. Компиляция модели внутри Ollama Запустите сервер Ollama обратно в фоне и выполните сборку: bash ``` ollama serve > /dev/null 2>&1 & cd /mnt/c/ST/aandc/ ollama create atm_expert -f ./Modelfile ``` Используйте код с осторожностью. _Ollama за секунды скопирует компоненты, переварит типы данных и напишет долгожданное `success`._ Шаг 6.3. Интерактивный запуск Входите в чат с готовым экспертом: bash ``` ollama run atm_expert ``` Используйте код с осторожностью. _Модель будет отвечать глубоко, инженерно точно, без колонтитулов и «каши». Выход из чата — команда `/exit`._ --- ЧАСТЬ 7. Автономный Перенос Эксперта на Другой ПК Поскольку модель собрана в монолит, перенос на любой другой компьютер (даже полностью отключенный от интернета) занимает ровно 1 минуту. 1. **Экспорт в GGUF-файл на текущем ПК:** Выполните в терминале WSL2 команду экспорта: bash ``` ollama show atm_expert --modelfile > /mnt/c/ST/aandc/atm_expert_final.gguf ``` Используйте код с осторожностью. _Заберите получившийся файл `atm_expert_final.gguf` (размером около 4.7 ГБ) на флешку._ 2. **Подготовка целевого ПК:** Установите чистую Ollama на новый компьютер (скачав десктопный установщик для Windows или Mac с официального сайта ollama.com). 3. **Импорт модели на новом месте:** Положите файл `atm_expert_final.gguf` в любую папку на новом ПК (например, `C:\ATM_AI\`). Создайте рядом текстовый файл `Modelfile` со следующим содержимым: dockerfile ``` FROM ./atm_expert_final.gguf SYSTEM "Вы — специализированный локальный ИИ-эксперт по банкоматному ПО. Вы отвечаете строго на основе технической документации, четко, структурированно, используя точные инженерные термины и коды ошибок." ``` Используйте код с осторожностью. Откройте обычную командную строку Windows (cmd или PowerShell) в этой папке и зарегистрируйте модель: powershell ``` cd C:\ATM_AI\ ollama create atm_expert -f ./Modelfile ``` Используйте код с осторожностью. 4. **Запуск:** Модель готова отвечать на новом ПК по стандартной команде: `ollama run atm_expert`. Если на целевом ПК нет видеокарты, обеспечьте ему минимум **16 ГБ оперативной памяти (RAM)** — Ollama автоматически и без сбоев переключит вычисления на процессор. ---