Основи донавчання за допомогою LoRA
Попередньо навчена модель мови вже містить корисні перетворення. Адаптація його до завдання означає зміну цих перетворень таким чином, щоб бажані відповіді стали більш імовірними. LoRA дозволяє нам вивчати ці зміни за допомогою невеликих пар матриць, зберігаючи попередньо підготовлені ваги замороженими.
Основна ідея полягає в тому, щоб обмежити ранг оновлення ваги. Оригінальна вагова матриця може зберігати свій повний ранг, а повна модель все ще використовує вихідні шари. Обмеження стосується поправки, яку ми вивчаємо для кожної обраної матриці.
Ми можемо зрозуміти це обмеження на двовимірному прикладі, а потім використовувати точно таку саму конструкцію всередині трансформатора. Попутно ми реалізуємо невеликий рівень LoRA в PyTorch і підключимо його розміри до адаптерів рангу 8 у нашому експерименті з тонкого налаштування JSON.
Тонка настройка вивчає зміну ваги
Почніть з лінійної проекції:
Вхід має значення , а вихід – значення . Кожен рядок зчитує шаблон із вхідних даних через скалярний добуток. У трансформаторі такі операції виконують запити, ключі, значення та проекції прямого зв’язку.
Повна тонка настройка дозволяє оновити вихідну матрицю. Ми можемо описати результат як , де — загальна зміна, накопичена за час навчання. Повне оновлення має окремий регульований запис для кожної ваги.
LoRA підтримує фіксованим і представляє виправлення як
Тут є коефіцієнтом масштабування, а контролює ширину адаптера. Навчання регулює і . Щоб побачити, що це нам дасть, нам спочатку знадобиться зображення рангу.
Ранг враховує незалежні напрямки виведення
Матриця перетворює вхідні дані, беручи лінійну комбінацію своїх стовпців. Простір стовпців — це набір усіх виходів, які може створити перетворення. Його розмірність - це ранг матриці.
Розгляньте це виправлення:
Його другий стовпець удвічі більший за перший. Обидва стовпці вказують уздовж напрямку , тому кожен можливий результат лежить на одній лінії. Він має чотири ненульові записи, але лише один незалежний напрямок виведення: його ранг дорівнює 1.
Для введення ,
Скаляр змінюється разом із входом, тоді як доступний вихідний напрямок залишається незмінним. Ранг 0 дасть лише нульовий вектор. Матриця рангу 2 у цьому двовимірному прикладі може створювати виходи по всій площині. Стаття про ранг матриці більш детально розробляє цю геометрію та її зв’язок із сингулярними значеннями.
Побудуйте цю корекцію з двох матриць
Вираз вище вже дає нам факторізацію:
спочатку читає скаляр із вхідних даних: . Тоді перетворює цей скаляр на вихідну корекцію вздовж . З це стає
Це зовнішній продукт рангу 1. Для ширшого адаптера кожен рядок зчитує один скалярний коефіцієнт, а кожен стовпець забезпечує вихідний напрямок:
Виправлення завжди лежить у діапазоні стовпців , отже
Межа може бути суворою: стовпці можуть бути залежними, фактор може містити нулі, або навчання може використовувати менше вказівок, ніж дозволяє адаптер. Ранг 8 означає, що виправлення має ранг не більше 8. Рядки і стовпці є вивченими напрямками; їхні координати можуть включати багато вхідних і вихідних функцій.
Оновлення рангу 1 може зберегти рівень рангу 2
Встановіть заморожену основу на матрицю ідентичності і використовуйте масштаб . Адаптована матриця є
Його визначником є , тому він має ранг 2. Виправлення відображає вхідні дані на лінії, тоді як повний шар все ще відображається на площині. Для нашого вибраного вхідного сигналу вихідний результат і корекція додаються до .
Спробуйте , і нижче. Перемістіть повзунки введення, щоб спостерігати за зміною і вибраного виходу. Це підібрані вручну матриці, тому ми можемо точно перевірити їхні ранги.
Correction alonerank 1
Selected output: (5, 10)
Complete layerrank 2
Selected output: (8, 11)
Both panels transform the same 49 input points and use the same axes. The large dot follows your selected input. Rank describes the whole matrix, even when one input produces a zero output. The correction scale is 1 in this illustration.
У ми додаємо другий незалежний зовнішній продукт:
Корекція тепер також може охоплювати площину. У всіх трьох попередніх налаштуваннях повний рівень зберігає ранг 2. Це відмінність, яка нам потрібна для LoRA: обмеження кожної корекції ваги не змушує повну попередньо підготовлену матрицю мати цей ранг. У багатьох адаптованих шарах і нелінійних операціях поведінка моделі може суттєво змінюватися.
Збереження параметрів з’являється при більших розмірах
Для щільного оновлення проекції потрібні значення , які можна навчити. Необхідні два фактори LoRA
Для проекції запиту Qwen із 896 входами та 896 виходами:
| Оновлення параметризації | Тренувані цінності |
|---|---|
| Оновлення Dense | 802,816 |
| Фактори рангу 8: і | 14,336 |
Це в 56 разів менше значень, які можна навчити для цієї проекції. Початкові 802 816 ваг матриці залишаються присутніми як заморожені вагові коефіцієнти.
Двовимірний приклад навмисно маленький: факторізація рангу 1 використовує параметри , таку саму кількість, як щільна матриця . Він навчає обмеженню рангу; велика проекція показує перевагу зберігання, коли малий порівняно з розмірами матриці.
Оригінальна стаття LoRA мотивує адаптацію низького рангу гіпотезою про те, що корисні зміни ваги, залежні від завдання, можуть займати набагато менше місця, ніж попередньо навчені ваги. Наскільки обмеженим є цей вибір, залежить від завдання та вибраних шарів. Вищий ранг дозволяє більш детальну корекцію, але кращу ефективність перевірки все одно потрібно виміряти.
Підключіть LoRA до декомпозиції сингулярного значення
Будь-яка матриця рангу має представлення SVD . Одним з дійсних факторів є і . Це дає інший спосіб зрозуміти, чому дві вузькі матриці можуть представляти поправку рангу .
Стандартне навчання LoRA вивчає фактори безпосередньо з втрати завдання. Він не спочатку навчає щільне оновлення, а потім скорочує його SVD, і не замінює оригінальну попередньо підготовлену матрицю на наближення низького рангу. Це були б різні процедури.
Фактори також не унікальні. Для оборотної матриці , . Зміна проміжної системи координат може зберегти ту саму поправку. Це одна з причин інтерпретувати комбіноване оновлення та його вплив на вхідні дані, а не призначати фіксоване семантичне значення одній координаті адаптера.
Додайте адаптер поруч із замороженою проекцією
Повний пас вперед є
Обидві гілки отримують однакові вхідні дані. Базова гілка зберігає попередньо підготовлене перетворення; гілка адаптера забезпечує вивчену корекцію. У цій конструкції немає активації між і , тому їхній продукт є лінійним оновленням вибраної проекції.
У стандартному LoRA, . У нашому прикладі трансформатора використовується ранг 8 і альфа 16, що дає . Альфа контролює цей явний масштаб; це окремо від швидкості навчання оптимізатора. Інші варіанти можуть використовувати інше правило масштабування, тому наведена тут формула описує стандарт LoRA у PEFT.
Почніть із попередньо навченої поведінки
Ініціалізація PEFT за замовчуванням лінійного рівня дає випадкові значення та встановлює на нуль. Адаптер спочатку надає нульовий внесок, тому рівень починається з попередньо навченого виводу. Під час першого проходу назад може отримати градієнт через випадковий . Градієнт втрати завдання для спочатку дорівнює нулю, оскільки він проходить через . Після зміни градієнти можуть тренувати обидва фактори. Ініціалізація обох факторів до нуля заблокує ці градієнти втрати завдань. Посібник з ініціалізації PEFT описує варіанти за замовчуванням і альтернативні варіанти.
Заморожені ваги все ще беруть участь у обчисленні. Зворотне розповсюдження має передавати навчальний сигнал через операції, необхідні для досягнення попередніх адаптерів. Заморожування ваги видаляє градієнт параметрів і оновлення оптимізатора; він не видаляє шар із моделі.
Впровадити та навчити невеликий адаптер у PyTorch
У наведеному нижче прикладі вивчається наша корекція рангу 1 на CPU без попереднього завантаження моделі. Ми генеруємо вхідні вектори та просимо шар відповідати . Середня квадратична помилка підходить для цієї невеликої задачі регресії. Мета полягає в тому, щоб спостерігати за тим, як фактори навчаються, поки вихідна матриця ідентичності залишається незмінною.
nn.Linear зберігає ваги форми (out_features, in_features). Для групи векторів-рядків PyTorch обчислює x @ weight.T; вкладені виклики F.linear, наведені нижче, реалізують вектор-стовпець за цією угодою.
Використовуйте для повноекранного перегляду. Показаний тут код є тим самим файлом, що й examples/lora_linear.py поруч із цією статтею.
"""Learn a rank-1 correction to a frozen linear layer on CPU. No LLM download."""
import copy
import json
import math
import torch
from torch import nn
from torch.nn import functional as F
class LoRALinear(nn.Module):
def __init__(self, base: nn.Linear, rank: int, alpha: float):
super().__init__()
self.base = base.requires_grad_(False)
self.scale = alpha / rank
self.A = nn.Parameter(torch.empty(rank, base.in_features))
self.B = nn.Parameter(torch.zeros(base.out_features, rank))
nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
def forward(self, x):
# F.linear(x, W) computes x @ W.T for a batch of row vectors.
correction = F.linear(F.linear(x, self.A), self.B)
return self.base(x) + self.scale * correction
@torch.no_grad()
def merged_copy(self):
merged = copy.deepcopy(self.base)
merged.weight.add_(self.scale * (self.B @ self.A))
return merged
def main():
torch.manual_seed(42)
torch.set_num_threads(1)
base = nn.Linear(2, 2, bias=False)
with torch.no_grad():
base.weight.copy_(torch.eye(2))
layer = LoRALinear(base, rank=1, alpha=1.0)
original_weight = base.weight.detach().clone()
x = torch.randn(256, 2)
desired_delta = torch.tensor([[1.0, 2.0], [2.0, 4.0]])
targets = F.linear(x, original_weight + desired_delta)
torch.testing.assert_close(layer(x), base(x)) # B starts at zero.
parameters = [p for p in layer.parameters() if p.requires_grad]
optimizer = torch.optim.AdamW(parameters, lr=0.05, weight_decay=0.0)
initial_loss = F.mse_loss(layer(x), targets).item()
for step in range(500):
optimizer.zero_grad(set_to_none=True)
loss = F.mse_loss(layer(x), targets)
loss.backward()
if step == 0:
first_A_gradient_norm = layer.A.grad.norm().item()
first_B_gradient_norm = layer.B.grad.norm().item()
assert first_A_gradient_norm == 0.0
assert first_B_gradient_norm > 0.0
optimizer.step()
with torch.no_grad():
delta = layer.scale * (layer.B @ layer.A)
adapted = original_weight + delta
final_loss = F.mse_loss(layer(x), targets).item()
merged = layer.merged_copy()
torch.testing.assert_close(delta, desired_delta, atol=1e-4, rtol=1e-4)
torch.testing.assert_close(merged(x), layer(x), atol=1e-5, rtol=1e-5)
assert torch.equal(base.weight, original_weight)
assert base.weight.grad is None
assert torch.linalg.matrix_rank(delta, atol=1e-5).item() == 1
assert torch.linalg.matrix_rank(adapted, atol=1e-5).item() == 2
print(json.dumps({
"device": str(x.device), "steps": 500,
"initial_mse": initial_loss, "final_mse": final_loss,
"first_A_gradient_norm": first_A_gradient_norm,
"first_B_gradient_norm": first_B_gradient_norm,
"base_unchanged": torch.equal(base.weight, original_weight),
"update_rank": 1, "adapted_weight_rank": 2,
"learned_delta": delta.tolist(),
}, indent=2))
if __name__ == "__main__":
main()З каталогу цієї статті скористайтеся uv, щоб підготувати середовище PyTorch лише для процесора та запустіть приклад:
uv run --no-project --python 3.12 \
--with torch==2.7.1 \
--index https://download.pytorch.org/whl/cpu \
examples/lora_linear.pyСценарій перевіряє, чи початковий адаптер зберігає вихідний вихід, перший градієнт для дорівнює нулю, перший градієнт для відмінний від нуля, а заморожена база ніколи не змінюється. Після навчання він перевіряє, чи вивчена поправка відповідає вибраній нами матриці, має ранг 1 і залишає повну вагову матрицю з рангом 2.
У нашому початковому циклі 42 середня квадратична помилка впала приблизно з 10,94 до . Вивчена корекція відповідала наведеній вище матриці з точністю FP32. Корисними перевірками є незмінна база, ранг поправки та узгодженість між окремими та об’єднаними обчисленнями.
У цій іграшці використовуються тензори ЦП FP32 і немає випадання чи квантування. В експерименті LLM ми використовуємо PEFT, щоб приєднати адаптери до фактичних проекцій та керувати інтеграцією моделі.
Втрата навчає завдання через адаптери
LoRA визначає, як ми параметризуємо зміни ваги. Завдання все ще відповідає меті навчання. У контрольованому тонкому налаштуванні мовної моделі це зазвичай крос-ентропія наступного токена:
Для нашого завдання JSON метою є правильна відповідь помічника. Позиції підказок і заповнення ігноруються, а втрата усереднюється за маркерами відповіді. Зворотне поширення обчислює градієнти для факторів адаптера, а AdamW оновлює ці параметри, які можна навчити. Немає окремої втрати, яка вимагає від матриці стати низького рангу: її факторизована форма вже забезпечує дотримання межі.
У прикладі ЦП використовується середня квадратична помилка, оскільки цільовими значеннями є числові вектори. В експерименті JSON використовується перехресна ентропія, оскільки його цілями є ідентифікатори маркерів. Конструкція адаптера сумісна з обома цілями.
Помістіть ту саму конструкцію всередину трансформаторних блоків
Наша закріплена контрольна точка Qwen2.5-0.5B-Instruct має 24 трансформаторні блоки та приховані вектори з 896 значеннями. Кожен блок містить проекції уваги та мережу прямого зв’язку. Ми адаптуємо прогнози запиту та значення всередині кожного блоку:
| Проекція | Оригінальна форма ваги | Ранг-8 Форма A | Ранг-8 форма B | Тренувані значення на блок |
|---|---|---|---|---|
q_proj | 14,336 | |||
v_proj | 8,192 |
Проекція значення є вужчою, оскільки ця модель використовує увагу згрупованого запиту: 14 заголовків запиту мають спільні дві заголовки ключ/значення, по 64 значення на заголовок. Результатом запиту є значення ; вихідне значення .
У всій моделі це дає параметри, які можна навчити. Існує 48 адаптованих проекцій, кожна зі своєю парою матриць. Їх поправки впливають на обчислення всередині трансформаторних блоків. Оригінальні ваги уваги, ваги прямої подачі, вбудовування та ваги нормалізації залишаються замороженими в цьому рецепті.
Відповідна конфігурація PEFT коротка:
LoraConfig(
task_type="CAUSAL_LM",
r=8,
lora_alpha=16,
lora_dropout=0.0,
target_modules=["q_proj", "v_proj"],
bias="none",
)Вибір проекцій запиту та значення є налаштуванням нашого експерименту. LoRA може націлюватися на інші лінійні проекції, включаючи шари прямого зв’язку. Ранг контролює розмір кожного обраного виправлення; список цільового модуля контролює, скільки виправлень ми додаємо та де вони діють.
Збережіть виправлення або об’єднайте його для висновку
Якщо відключення вимкнено, дві гілки можна комбінувати алгебраїчно:
Сценарій центрального процесора створює об’єднану копію та перевіряє, чи дає вона однакові виходи з толерантністю до числа з плаваючою комою. Він залишає оригінальну заморожену основу недоторканою. У підтримуваній моделі PEFT без квантування merge_and_unload() поєднує адаптер із основою для висновку, видаляючи окрему гілку адаптера.
Збереження необ’єднаного адаптера зберігає виправлення невеликим і дозволяє завантажувати його разом із сумісною попередньо підготовленою контрольною точкою. Адаптер не містить повної базової моделі; її конфігурація та правильна базова версія є частиною того, що нам потрібно для відтворення адаптованої моделі. Див. Формат контрольної точки PEFT.
Що купує нам менша кількість параметрів
Менша кількість тренуваних ваг означає менше градієнтів параметрів і записів стану оптимізатора, а також меншу контрольну точку для конкретного завдання. Базові ваги та активації, необхідні для навчання, все ще споживають пам’ять. Одна вага нашого адаптера 540 672 FP32 займає близько 2,06 Мбайт, тоді як виміряна розгортка Qwen microbatch-4 використовувала приблизно 3,82 Гібайт пікової виділеної пам’яті GPU для повного робочого навантаження.
Ранг і розмір партії контролюють різні ресурси. Ранг змінює форму адаптера та репрезентативну здатність. Розмір мікропартії змінює кількість прикладів, які ми обробляємо разом, і обсяг пам’яті для активації, який нам потрібен. Підвищення рангу не покращує автоматично точність утримування, а зменшення параметрів, які можна тренувати, не означає такого ж скорочення часу навчання.
Для цього методу корисною картиною є попередньо підготовлене перетворення плюс корекція, побудована з кількох вхідних вимірювань і вихідних напрямків. Ми зберігаємо вихідну матрицю, вивчаємо фактори та оцінюємо, чи ця обмежена зміна вирішує завдання. Експеримент із тонким налаштуванням JSON поєднує ці фрагменти разом із реальними запитами, втратою маркерів, збереженими адаптерами та вимірюваннями GPU.