Files
obsidian/🤖 ЛОКАЛЬНЫЙ ИИ-ЭКСПЕРТ ПО БАНКОМАТНОМУ ПО.md

26 KiB
Raw Permalink Blame History

  • Платформа: 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

Используйте код с осторожностью.

  1. Перезапустите 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 автоматически и без сбоев переключит вычисления на процессор.