Обучих локален AI с физически текстури от работилницата

Когато алгоритъмът се научи да вижда метал
1. Войната срещу пластмасовия AI
Днешният AI има проблем с материята. Поискай му индустриална сцена и често ще получиш метал, който изглежда така, сякаш никога не е докосвал работна маса. Перфектни повърхности, декоративна ръжда, стерилни искри и заваръчни шевове, изстискани като паста за зъби.
Изображението може да е технически впечатляващо, но физически е празно.
Алгоритъмът не знае как тежи стоманата. Не е усещал мириса на нагорещен метал, не е чувал как ламарината променя звука си преди да поддаде и не различава истински термичен отпечатък от кафяв филтър с име „rust“.
Той не разбира материята. Само предсказва как би трябвало да изглеждат следващите пиксели.
Затова спрях да го моля да си представя моя свят. Вместо това му показах доказателствата.
Съвършенството учи машината да прави реклама. Белезите я учат да разказва история.
2. Суровият набор от данни
Взех камерата и заснех работилницата такава, каквато е - без филтри, без изгладени повърхности и без дигитална пудра. Но „сурово“ не означава небрежно. Фокусът, светлината и експозицията трябваше да позволят на модела да прочете структурата на материала, а не само да види кафяво, сиво и черно.
Наборът беше изграден около четири визуални семейства:
- weld_seam_01 - Плътен TIG шев върху стомана с реални термични оцветявания, локално окисление, следи от обработка и неидеално равномерна геометрия. Истинският TIG шев не разказва историята си чрез декоративни пръски, а чрез зоната на термично влияние, чистотата на материала и качеството на газовата защита.
- oxidized_plate - Стоманена повърхност с дълбока ръжда, напукан лак, драскотини и слоеве от офроуд проект, преживял повече кал, отколкото маркетинговият отдел би одобрил.
- terminal_dark - Черен AMOLED екран с реална йерархия от Linux kernel логове, студена светлина, отражения и почти невидим прах върху стъклото. Терминалът не е декоративен HUD, а част от физическото пространство.
- grinder_spark - Искри от ъглошлайф, заснети в движение, с различна дължина, яркост и траектория. Не оранжево конфети, а частици метал, изхвърлени от триенето.
Над 50 кадъра във всяко семейство. Над 200 изображения общо, всичките заснети в една работилница.
Числото не е самоцел. Петдесет снимки на един и същи шев от един и същи ъгъл са петдесет пъти една снимка. Петдесет шева при различна светлина, различна стомана, различна възраст на окислението и различно разстояние на камерата са вече набор от данни.
Всяка група обучаваше различна част от визуалния език - повърхност, стареене, излъчвана светлина и движение. Една снимка учи модела на един предмет. Серията от различни ъгли и светлинни условия започва да изгражда речник.
3. Caption-ите: Разликата между стил и информационна каша
Използвах постоянен trigger token:
Code:
dtg_industrial
Останалата част от описанието се променяше според конкретното изображение:
- dtg_industrial, TIG weld bead on steel, heat tint, oxidized surface, workshop lighting
- dtg_industrial, corroded steel plate, cracked paint, deep rust texture, off-road wear
- dtg_industrial, Linux terminal on dark AMOLED display, cold screen light, dusty glass
- dtg_industrial, angle grinder sparks, dark workshop, moving metal particles
Това разделение е критично. Ако всяко изображение бъде описано едновременно с „weld“, „rust“, „terminal“ и „sparks“, моделът няма да научи четири понятия. Той ще ги слепи в една визуална супа и ще започне да поставя терминални символи в ръждата или заваръчни шевове върху всичко, което попадне в кадъра.
Уникалният token пази идентичността на стила. Точният caption пази значението на отделните елементи.
4. Обучение под Linux: No Cloud, Pure Local
Целият процес протече локално. Нито оригиналните изображения, нито caption-ите, нито междинните checkpoint-и напуснаха машината.
Работният стек
- Debian GNU/Linux
- ComfyUI
- kohya_ss / sd-scripts
- FLUX.1-dev като базова архитектура, избрана предварително
- LoRA weights във формат .safetensors
Тук има една подробност, която често се пропуска: LoRA, обучена за SDXL, не се превръща магически в LoRA за FLUX.1. Двете архитектури изискват отделно обучение, отделен network module и съвместими настройки. Файловото разширение може да изглежда еднакво, но вътрешната карта не е една и съща.
При този експеримент използвах 1500 training steps и learning rate 1e-4. Тези числа обаче не значат нищо, откъснати от размера на набора.
При batch size 1 и над 200 изображения, 1500 стъпки означават, че всеки кадър е бил видян около седем пъти. Това е коренно различен режим от 20 снимки, набивани по 75 пъти всяка. Големият и разнообразен набор ти дава широчина вместо повторение, затова и рискът от свръхобучение пада, без да качваш стъпките.
Сметката е проста и си струва да се направи преди обучението, а не след него:
Code:
преглеждания на изображение = (steps × batch_size) / брой изображения
steps за целева стойност = (преглеждания × изображения) / batch_size
набор: 200 изображения
конфигурация steps batch преглеждания/изобр.
---------------------------------------------------------
този експеримент 1500 1 7.5
същите steps, batch 2 1500 2 15.0
същите steps, batch 4 1500 4 30.0
конфигурация 20 × 10 1500 1 75.0
(20 изображения, 10 repeats)
за 7.5 преглеждания при batch 2 -> 750 steps
за 7.5 преглеждания при batch 4 -> 375 steps
Последните два реда са мястото, където се бъркат най-много хора. Вдигането на batch size не е безплатно ускорение - при същия брой стъпки моделът вижда всяко изображение два или четири пъти повече. Ако вдигнеш batch-а и оставиш стъпките, не си тренирал по-бързо, а си тренирал повече, отколкото си искал.
И още едно уточнение: при по-голям batch обикновено се качва и learning rate. Двете се движат заедно, но не по едно и също правило за всеки optimizer, така че тази таблица дава стъпките, не тока и подаването наведнъж.
Реалният баланс зависи от броя изображения, повторенията, batch size, optimizer-а, network rank-а и разнообразието на набора. Всяка от тези променливи мести останалите.
Броячът на стъпките не казва дали моделът е готов. Казват го резултатите.
Признаците за свръхобучение се виждат лесно:
- една и съща ръжда започва да се повтаря върху различни обекти;
- моделът копира композицията на тренировъчните снимки;
- заваръчни следи се появяват дори когато prompt-ът не ги изисква;
- trigger token-ът започва да задушава останалата част от описанието.
Целта не беше моделът да запомни една ламарина. Целта беше да разбере как времето пише върху метала.
5. Скриптът, който върши работата
Не пускам обучение на ръка от терминала. Твърде много аргументи, твърде лесно се пропуска един и се разбира три часа по-късно.
Няколко неща в този скрипт заслужават внимание, преди да го копираш:
- FLUX.1 не се зарежда от един файл. Трансформерът, CLIP-L, T5-XXL и автоенкодерът се подават като четири отделни пътя.
- Модулът е networks.lora_flux, не networks.lora. Това е точно разликата, за която стана дума по-горе.
- FLUX е flow matching модел. Без timestep_sampling, discrete_flow_shift, model_prediction_type и guidance_scale тренираш нещо друго.
- Датасетът стои в поддиректории от вида <повторения>_<име>. Папка направо със снимки дава нула кадъра.
- Seed-ът се записва. Без него сравнението „преди и след“ не е сравнение, а анекдот.
Code:
#!/usr/bin/env bash
# ==============================================================================
# DTGaraGe Industrial LoRA Training Script
# System: Debian GNU/Linux | Target: FLUX.1-dev (LoRA, unet only)
# Author: toni@dtgarage
# ==============================================================================
set -euo pipefail
# ------------------------------------------------------------------------------
# 1. СРЕДА И ПЪТИЩА
# ------------------------------------------------------------------------------
VENV_DIR="/opt/kohya_ss/venv"
KOHYA_DIR="/opt/kohya_ss/sd-scripts"
# FLUX.1 се зарежда от четири отделни файла, не от един чекпойнт
MODEL_PATH="/models/unet/flux1-dev.safetensors"
CLIP_L_PATH="/models/clip/clip_l.safetensors"
T5XXL_PATH="/models/clip/t5xxl_fp16.safetensors"
AE_PATH="/models/vae/ae.safetensors"
# Структура: DATASET_DIR/<repeats>_<concept>/*.jpg + *.txt
# напр. /datasets/dtg_industrial/1_weld_seam/, 1_oxidized_plate/ и т.н.
DATASET_DIR="/datasets/dtg_industrial"
OUTPUT_DIR="/models/loras"
LOGS_DIR="${HOME}/kohya_logs"
OUTPUT_NAME="dtg_rust_and_steel"
# ------------------------------------------------------------------------------
# 2. ПАРАМЕТРИ НА ОБУЧЕНИЕТО
# ------------------------------------------------------------------------------
LEARNING_RATE="1e-4"
MAX_TRAIN_STEPS=1500
SAVE_EVERY_N_STEPS=500
BATCH_SIZE=1
NETWORK_DIM=32
NETWORK_ALPHA=16
MIXED_PRECISION="bf16" # bf16 за Ampere+; fp16 за по-стари карти
RESOLUTION="1024,1024"
SEED=42
# FLUX-специфични (flow matching, не DDPM)
TIMESTEP_SAMPLING="shift"
DISCRETE_FLOW_SHIFT="3.1582"
MODEL_PREDICTION_TYPE="raw"
GUIDANCE_SCALE="1.0"
# ------------------------------------------------------------------------------
# 3. ИНИЦИАЛИЗАЦИЯ И ПРОВЕРКИ
# ------------------------------------------------------------------------------
echo "=========================================================="
echo " DTGaraGe Industrial LoRA Compilation Engine"
echo "=========================================================="
fail() { echo "[!] ERROR: $*" >&2; exit 1; }
[ -d "$VENV_DIR" ] || fail "Virtualenv not found at $VENV_DIR"
[ -f "$MODEL_PATH" ] || fail "Base model not found at $MODEL_PATH"
[ -f "$CLIP_L_PATH" ] || fail "CLIP-L not found at $CLIP_L_PATH"
[ -f "$T5XXL_PATH" ] || fail "T5-XXL not found at $T5XXL_PATH"
[ -f "$AE_PATH" ] || fail "AE not found at $AE_PATH"
[ -d "$DATASET_DIR" ] || fail "Dataset not found at $DATASET_DIR"
# kohya брои изображения само в поддиректории <repeats>_<name>
if ! find "$DATASET_DIR" -maxdepth 1 -type d -regextype posix-extended \
-regex '.*/[0-9]+_.*' | grep -q .; then
fail "$DATASET_DIR няма поддиректории от вида <repeats>_<concept>"
fi
IMG_COUNT=$(find "$DATASET_DIR" -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) \
| wc -l)
[ "$IMG_COUNT" -gt 0 ] || fail "Нула изображения в $DATASET_DIR"
PASSES=$(awk -v s="$MAX_TRAIN_STEPS" -v b="$BATCH_SIZE" -v n="$IMG_COUNT" \
'BEGIN { printf "%.1f", (s * b) / n }')
echo "[i] Images: $IMG_COUNT"
echo "[i] Steps: $MAX_TRAIN_STEPS | batch: $BATCH_SIZE | LR: $LEARNING_RATE"
echo "[i] Преглеждания на изображение: ~$PASSES"
echo "[i] Output: $OUTPUT_DIR/$OUTPUT_NAME.safetensors"
mkdir -p "$OUTPUT_DIR" "$LOGS_DIR"
# някои activate скриптове реферират неинициализирани променливи
set +u
# shellcheck source=/dev/null
source "$VENV_DIR/bin/activate"
set -u
cd "$KOHYA_DIR"
trap 'echo "[!] Training failed. Логове: $LOGS_DIR" >&2' ERR
# ------------------------------------------------------------------------------
# 4. ИЗПЪЛНЕНИЕ
# ------------------------------------------------------------------------------
echo "[i] Status: Starting LoRA training loop..."
accelerate launch \
--num_cpu_threads_per_process=8 \
--mixed_precision="$MIXED_PRECISION" \
flux_train_network.py \
--pretrained_model_name_or_path="$MODEL_PATH" \
--clip_l="$CLIP_L_PATH" \
--t5xxl="$T5XXL_PATH" \
--ae="$AE_PATH" \
--train_data_dir="$DATASET_DIR" \
--output_dir="$OUTPUT_DIR" \
--output_name="$OUTPUT_NAME" \
--logging_dir="$LOGS_DIR" \
--log_prefix="${OUTPUT_NAME}_" \
--save_model_as=safetensors \
--save_precision="$MIXED_PRECISION" \
--mixed_precision="$MIXED_PRECISION" \
--network_module=networks.lora_flux \
--network_dim="$NETWORK_DIM" \
--network_alpha="$NETWORK_ALPHA" \
--network_train_unet_only \
--learning_rate="$LEARNING_RATE" \
--optimizer_type=adamw8bit \
--lr_scheduler=cosine \
--max_train_steps="$MAX_TRAIN_STEPS" \
--save_every_n_steps="$SAVE_EVERY_N_STEPS" \
--train_batch_size="$BATCH_SIZE" \
--resolution="$RESOLUTION" \
--enable_bucket \
--bucket_reso_steps=64 \
--bucket_no_upscale \
--min_bucket_reso=512 \
--max_bucket_reso=1536 \
--seed="$SEED" \
--sdpa \
--gradient_checkpointing \
--fp8_base \
--cache_latents \
--cache_latents_to_disk \
--cache_text_encoder_outputs \
--cache_text_encoder_outputs_to_disk \
--persistent_data_loader_workers \
--max_data_loader_n_workers=4 \
--timestep_sampling="$TIMESTEP_SAMPLING" \
--discrete_flow_shift="$DISCRETE_FLOW_SHIFT" \
--model_prediction_type="$MODEL_PREDICTION_TYPE" \
--guidance_scale="$GUIDANCE_SCALE"
# ------------------------------------------------------------------------------
# 5. ЗАВЪРШВАНЕ
# ------------------------------------------------------------------------------
trap - ERR
echo "=========================================================="
echo "[+] SUCCESS: LoRA compiled."
echo "[+] Path: $OUTPUT_DIR/$OUTPUT_NAME.safetensors"
echo "[+] Seed: $SEED (запази го за before/after сравнението)"
echo "=========================================================="
Две неща зависят от твоята машина и няма как да ги предпиша отдалеч. Флагът --fp8_base иска карта с fp8 поддръжка. На по-стар хардуер го махни и добави --blocks_to_swap 10, ако видеопаметта не стига. А adamw8bit изисква bitsandbytes във виртуалната среда; ако го няма, adafactor върши работа без него.
Скриптът брои изображенията сам и ти казва колко пъти ще види всяко от тях, преди да е тръгнал. По-добре е от таблица в статия, защото смята от реалната папка, а не от предположение.
6. Когато алгоритъмът разбра занаята
Резултатът не е просто още един „индустриален стил“. LoRA започна да възпроизвежда визуална логика, която липсваше в стандартните генерации - неравномерно окисление, драскотини с различна дълбочина, термични зони около шева, замърсяване, матови участъци и отражения, които не приличат на автомобилен шоурум.
Металът вече не изглеждаше боядисан като стар. Изглеждаше така, сякаш има минало.
Искрите престанаха да бъдат оранжев декоративен ефект. Терминалът вече стоеше като студен източник на светлина в реална работилница, а не като холограма, залепена върху изображението от някой дизайнер, гледал твърде много евтин киберпънк.
Визуалното ядро на основния банер беше създадено именно с тази LoRA. Четимите терминални надписи обаче трябва да останат отделен и прецизен слой. Генераторът може да научи текстурата на екрана, но не бива да му се възлага технически текст. Иначе рано или късно ще сложи шестица на мястото на буквата „б“ и цялата магия ще рухне като евтина ламарина.
7. Какво моделът не е научил
LoRA не разбира материалознание. Тя не може да определи дали един шев е здрав, дали проварът е достатъчен или дали конструкцията ще издържи натоварването. Визуалната достоверност не е техническа инспекция.
Моделът научи езика на следите, но знанието за материала остава при човека.
И точно тук е границата между занаятчията и генератора.
Машината може да възпроизведе белега. Само човекът знае как се е появил, какво означава и дали детайлът трябва да бъде оставен, поправен или изрязан.
8. Бунтът на занаятчията
Повечето хора използват AI, за да избягат от реалността в безупречни измислени светове. Аз направих обратното - взех праха, ръждата, термичните петна и светлината от терминала и ги инжектирах обратно в цифровата матрица.
Не отхвърлям алгоритъма. Но не му се покланям като на оракул.
Настройвам го като машина - подаване, ток, газ, дистанция, наблюдение. Ако резултатът е пластмасов, проблемът невинаги е в модела. Понякога операторът му е дал стерилни данни и после се чуди защо получава стерилен свят.
Истинският дигитален просветител не консумира готови алгоритми. Той ги прекарва през собствения си опит, докато започнат да говорят на език, който интернет базите данни никога не са познавали.
Не дадох на модела стил. Дадох му доказателства, че материята е живяла.
Code:
toni@dtgarage:~$ ./train_lora.sh --profile dtg_industrial
status: training completed
checkpoint: /models/loras/dtg_rust_and_steel.safetensors
cloud_upload: disabled
reality_injection: successful
Ключови думи: LoRA обучение, локален AI, FLUX.1, SDXL, kohya_ss, sd-scripts, ComfyUI, Debian, Linux, bash скрипт, accelerate, .safetensors, trigger token, captions, dataset, network rank, batch size, свръхобучение, TIG заваряване, провар, зона на термично влияние, окисление, ръжда, индустриална фотография, генеративен AI, self-hosted, no cloud
Инструменти и препратки: Debian GNU/Linux (debian.org) | ComfyUI (github.com/comfyanonymous/ComfyUI) | kohya_ss sd-scripts (github.com/kohya-ss/sd-scripts) | документация за flux_train_network (github.com/kohya-ss/sd-scripts/blob/main/docs/flux_train_network.md) | Black Forest Labs FLUX.1
Всички изображения от набора са заснети лично в работилницата на DTGaraGe. Нито едно от тях не е публикувано или качено в облачна услуга.
toni@dtgarage:~$ whoami
Тони Ангелчовски | Ексклузивно за DTGaraGe
toni@dtgarage:~$ cat LICENSE
Копирането и препубликуването без разрешение не е позволено.
toni@dtgarage:~$ ./support.sh
Подкрепи DTGaraGe и независимото техническо съдържание
toni@dtgarage:~$ █
Тони Ангелчовски | Ексклузивно за DTGaraGe
toni@dtgarage:~$ cat LICENSE
toni@dtgarage:~$ ./support.sh
toni@dtgarage:~$ █
Last edited: