tez10 va tez11: qayta training qayerda to'xtadi
Bitta o'zgarish plani
21-bob shu plan bilan tugadi: tez10 = tez8 ning aynan o'zi + faqat 858 ta "qila olmayman" qatori. tez9 da oltita narsa birga o'zgargani uchun sababni bilib bo'lmagan edi. Endi bitta o'zgarish — va natija nima uchun shunday ekanini aniq bilamiz, degan umid.
Avval ma'lumotni tekshiramiz. Ikkita training faylini qatorma-qator solishtiramiz:
import json from collections import Counter def qatorlar(model): return [json.loads(q) for q in open(Path.home() / f"tez_runs/{model}/data/train.jsonl", encoding="utf-8")] t8, t10 = qatorlar("tez8"), qatorlar("tez10") print(f"tez8: {len(t8):,} qator, tez10: {len(t10):,} qator, farq: {len(t10) - len(t8):,}") def kalit(qator): return json.dumps(qator["messages"][1:], ensure_ascii=False) def tizim(qator): return qator["messages"][0]["content"] savol8 = {kalit(q): tizim(q) for q in t8} umumiy = [q for q in t10 if kalit(q) in savol8] print(f"tez8 ning so'rov va javoblari tez10 da: {len(umumiy):,} ta") print(f"ulardan tizim prompti ham aynan bir xil: {sum(tizim(q) == savol8[kalit(q)] for q in umumiy):,} ta")
tez8: 19,886 qator, tez10: 20,744 qator, farq: 858 tez8 ning so'rov va javoblari tez10 da: 19,886 ta ulardan tizim prompti ham aynan bir xil: 0 ta
So'rovlar va javoblar — aynan tez8 niki. Lekin tizim prompti bittasida ham bir xil emas. Qaysi qator farq qiladi?
birinchi = umumiy[0] eski = savol8[kalit(birinchi)].splitlines() yangi = tizim(birinchi).splitlines() for a, b in zip(eski, yangi): if a != b: print("tez8 :", a) print("tez10:", b)
tez8 : - group_and_total(group_by: columns, value_column: column, agg: sum|count|avg|min|max, share: boolean?): Group rows by columns and aggregate a value column. share adds each group's percent of the total tez10: - group_and_total(group_by: columns?, value_column: column, agg: sum|count|count_distinct|avg|min|max, share: boolean?): Group rows by columns and aggregate a value column. Without group_by it gives one total for all rows. count_distinct counts different values. share adds each group's percent of the total
group_and_total toolining ta'rifi. tez9 uchun toollar fayli o'zgargan edi (butun jadval bo'yicha jami),
tez10 ning asosi esa shu yangi fayl bilan qayta eksport qilingan. Natijada tez10 da bitta emas, ikkita
o'zgarish bor: 858 ta yangi qator va har bir qatordagi yangi tool ta'rifi. Bu farq trainingdan keyin, natija
tahlilida topildi. Xatolar jurnalida — 21-raqam:
def jurnal(raqam): for qator in Path("/mnt/c/Users/Admin/Desktop/Offline/12-mistakes-and-lessons.md").read_text(encoding="utf-8").splitlines(): if qator.startswith(f"| {raqam} |"): return [q.strip() for q in qator.strip("|").split("|")] for ustun, matn in zip(("xato", "narxi", "qoida"), jurnal(21)[2:]): print(f"{ustun}: {matn}\n")
xato: Called tez10 "tez8's data plus 858 refusal rows" (plan 14, book chapter 29) without diffing the two training files. The base was re-exported after the tools.json change, so the `group_and_total` description line differs from tez8's in all 19,886 rows; requests, answers and tables are identical. narxi: tez10 carries two changes, not one, and the single-change rule from mistake 16 was broken without anyone noticing until after the benchmark. qoida: Before training, diff the new training file against the current model's file field by field and write every difference in the run note.
Qoida oddiy: trainingdan oldin yangi faylni joriy model fayli bilan maydonma-maydon solishtirish va har bir farqni yozib qo'yish.
Natija
tez10 ham 745 savolda, tez8 bilan bir xil grammar va bir xil hakam bilan sinaldi:
import glob import random TOZA = ["loans", "deposits", "cash_desk", "transactions", "realistic_loans", "realistic_deposits", "realistic_cash"] GURUHLAR = { "toza": TOZA, "shovqinli": [f"noisy_{t}" for t in TOZA], "real": sorted(Path(f).parent.name for f in glob.glob(str(TEZ / "accuracy/suites/real_*/questions.json"))), } HAMMASI = sum(GURUHLAR.values(), []) def hukmlar(prefiks, toplamlar): return {(t, j["id"]): j for t in toplamlar for j in map(json.loads, open(TEZ / f"results/{prefiks}_cpu_{t}.jsonl", encoding="utf-8"))} def oraliq(prefiks_a, prefiks_b, toplamlar, marta=10000): a, b = hukmlar(prefiks_a, toplamlar), hukmlar(prefiks_b, toplamlar) farqlar = [int(b[k]["correct"]) - int(a[k]["correct"]) for k in sorted(a)] tasodif = random.Random(1) yigindilar = sorted(sum(tasodif.choice(farqlar) for _ in farqlar) for _ in range(marta)) return sum(farqlar), yigindilar[int(0.025 * marta)], yigindilar[int(0.975 * marta)] for guruh, toplamlar in GURUHLAR.items(): a, b = hukmlar("tez8g", toplamlar), hukmlar("tez10g", toplamlar) print(f"{guruh:10s} {len(a):4d} savol: tez8 {sum(j['correct'] for j in a.values())}, tez10 {sum(j['correct'] for j in b.values())}") for nom, toplamlar in (("real", GURUHLAR["real"]), ("hammasi", HAMMASI)): farq, past, yuqori = oraliq("tez8g", "tez10g", toplamlar) print(f"{nom:8s} tez10 - tez8 = {farq:+d}, 95% oraliq [{past:+d}, {yuqori:+d}]")
toza 320 savol: tez8 315, tez10 307 shovqinli 320 savol: tez8 311, tez10 309 real 105 savol: tez8 81, tez10 82 real tez10 - tez8 = +1, 95% oraliq [-5, +7] hammasi tez10 - tez8 = -9, 95% oraliq [-20, +1]
Jami bo'yicha tez10 tez8 dan to'qqiz savolga past — xuddi tez9 kabi. Real jadvallarda esa biroz yuqori, lekin oraliq nolni o'z ichiga oladi.
Faqat to'g'ri javoblar sonini sanash bu yerda yetarli emas. Tez uchun ikkita savol muhimroq: model javob berganda qanchalik ishonchli (precision — aniqlik), va berishi kerak bo'lgan savolni qanchalik beradi (ask recall — savolni topish ulushi):
def matritsa(prefiks, toplamlar): sanoq = Counter(j["reason"] if not j["reason"].startswith("plan failed") else "plan failed" for j in hukmlar(prefiks, toplamlar).values()) javoblar = sum(v for k, v in sanoq.items() if k not in ("asked", "asked on a clear request")) savollar = sanoq["asked"] + sanoq["guessed instead of asking"] return sanoq["same result"], javoblar, sanoq["asked"], savollar for prefiks in ("tez8g", "tez10g"): togri, javoblar, topildi, savollar = matritsa(prefiks, GURUHLAR["real"]) print(f"{prefiks[:-1]:6s} real: precision {togri}/{javoblar} = {togri / javoblar:.1%}, ask recall {topildi}/{savollar}")
tez8 real: precision 75/99 = 75.8%, ask recall 6/15 tez10 real: precision 71/90 = 78.9%, ask recall 11/15
Mana tez10 ning haqiqiy yutug'i: real jadvallarda javob berganda u tez8 dan aniqroq, va berishi kerak bo'lgan savollarning ko'pini beradi. "Qila olmayman" qatorlari ishladi. Lekin jami bo'yicha baribir past — boshqa joylarda to'g'ri javoblar yo'qoldi.
858 qatorning shakli
Nega "qila olmayman" ko'nikmasi yarim-yarti ishladi? Qatorlarning o'zini ko'raylik — tez10 ga qo'shilgan
858 ta so'rov va real testdagi tool_gap tuzoqlari uzunligi (so'zlarda):
yangi = [json.loads(q) for q in open(Path.home() / "tez_runs/tez11/data/new.jsonl", encoding="utf-8")] qator_soz = Counter(len(q["messages"][-2]["content"].split()) for q in yangi) test_soz = Counter(len(s["text"].split()) for t in GURUHLAR["real"] for s in json.load(open(TEZ / f"accuracy/suites/{t}/questions.json", encoding="utf-8")) if s.get("trap") == "tool_gap") print(f"o'qitish qatorlari: {len(yangi)} ta, eng qisqasi {min(qator_soz)} so'z, eng uzuni {max(qator_soz)} so'z") print(f" 10-16 so'zli: {sum(v for k, v in qator_soz.items() if 10 <= k <= 16)} ta") print(f"test tool_gap savollari: {sum(test_soz.values())} ta, {min(test_soz)} dan {max(test_soz)} so'zgacha") print("namuna:", yangi[0]["messages"][-2]["content"])
o'qitish qatorlari: 858 ta, eng qisqasi 6 so'z, eng uzuni 36 so'z 10-16 so'zli: 586 ta test tool_gap savollari: 6 ta, 4 dan 8 so'zgacha namuna: Kelgusi chorakda 'Logistika' sohasi uchun 'Kredit liniyasi' mahsuloti bo'yicha kutilgan jami summan taxmin qilish mumkinmi?
Training qatorlari uzun, rasmiy jumlalar. Test savollari esa — foydalanuvchi qanday yozsa shunday: qisqa.
Model "qila olmayman"ni faqat uzun, rasmiy so'rovlarda o'rgandi. Bu 6-xatoning takrori edi — jurnalda
22-raqam (jurnalda test savollari "3 dan 8 so'zgacha" deb yozilgan; bu yerda o'lchangan oltita real
tool_gap savoli — 4 dan 8 gacha; xulosa o'zgarmaydi):
for ustun, matn in zip(("xato", "narxi", "qoida"), jurnal(22)[2:]): print(f"{ustun}: {matn}\n")
xato: The 858 refusal rows went into tez10 without the length check from mistake 6: none is shorter than 6 words, most are 10 to 16, while the test tool-gap questions are 3 to 8 words. narxi: The gate's short-request gaps moved the wrong way (4-5 words 21% to 20%, 1-3 words 16% to 15%); the model saw refusals only in long, formal phrasing. qoida: Every added row set, refusals included, passes the coverage gate on its own before it joins the training file.
Javoblar almashinuvi
tez9 va tez10 — ikkalasi ham to'qqiz savolga past. Tasodifmi? Ikkalasida ham asos tez8 ning ma'lumoti, o'zgarish esa kichik. Har bir yangi training tez8 ning javoblaridan qanchasini o'zgartiradi?
for prefiks in ("tez9g", "tez10g"): a, b = hukmlar("tez8g", HAMMASI), hukmlar(prefiks, HAMMASI) ozgardi = sum(a[k]["output"] != b[k]["output"] for k in a) buzildi = sum(a[k]["correct"] and not b[k]["correct"] for k in a) tuzaldi = sum(b[k]["correct"] and not a[k]["correct"] for k in a) print(f"{prefiks[:-1]:6s}: {ozgardi} ta javob o'zgardi ({ozgardi / len(a):.1%}), {tuzaldi} tasi tuzaldi, {buzildi} tasi buzildi")
tez9 : 128 ta javob o'zgardi (17.2%), 8 tasi tuzaldi, 17 tasi buzildi tez10 : 120 ta javob o'zgardi (16.1%), 10 tasi tuzaldi, 19 tasi buzildi
Bu hodisaning nomi — churn (javoblar almashinuvi). Har bir qayta training javoblarning taxminan oltidan birini o'zgartiradi, ba'zisini to'g'riga, ba'zisini noto'g'riga. Toza to'plamlardagi ship chegarasi esa (320 dan 310) shu almashinuv ichida turardi: oddiy qayta trainingning o'zi uni kesib o'tishi mumkin edi. Jurnalda — 23-raqam:
for ustun, matn in zip(("xato", "narxi", "qoida"), jurnal(23)[2:]): print(f"{ustun}: {matn}\n")
xato: Set the clean ship bar (310 of 320) without measuring how much a plain retrain changes. Older consecutive models changed 16 to 18% of outputs and broke 1 to 5 of 200 answers with near-identical data; tez8 sits at 315, so that churn alone can cross the bar. narxi: tez9 and tez10 both missed clean by 7 to 8 answers, a size the churn baseline can produce on its own; the bar cannot tell a harmful change from an ordinary retrain. qoida: Measure retrain churn with a control run before setting a regression bar, and set the bar as a margin above that churn.
tez11: noldan emas, davomidan
Agar har bir noldan training javoblarning oltidan birini o'zgartirsa, yechim ko'rinadi: noldan o'qitmaslik. tez8 ning tayyor adapter ini olib, uni yangi qatorlar bilan davom ettirish — continuation training (davomli training). Eski ko'nikmalar unutilmasligi uchun yangi qatorlarga tez8 ning eski qatorlaridan tasodifiy namuna qo'shiladi — replay (takrorlash). O'quv tezligi (learning rate) esa noldan trainingnikidan to'rt barobar past.
Buning uchun alohida skript yozildi, training/update.py. Uning yuragi — ikki kichik funksiya:
import ast manba = (TEZ / "training/update.py").read_text(encoding="utf-8") for tugun in ast.parse(manba).body: if isinstance(tugun, ast.FunctionDef) and tugun.name in ("mix", "load_model"): print(ast.get_source_segment(manba, tugun), end="\n\n")
def mix(new, replay, replay_rows, seed):
rng = random.Random(seed)
rows = [*new, *rng.sample(replay, min(replay_rows, len(replay)))]
rng.shuffle(rows)
return rows
def load_model(base, adapter, quantization):
import torch
from peft import PeftModel, prepare_model_for_kbit_training
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
quant = BitsAndBytesConfig(**{**quantization, "bnb_4bit_compute_dtype": getattr(torch, quantization["bnb_4bit_compute_dtype"])})
model = AutoModelForCausalLM.from_pretrained(base, quantization_config=quant, dtype=torch.bfloat16, device_map={"": 0})
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
return PeftModel.from_pretrained(model, adapter, is_trainable=True)
mix — yangi qatorlar va replay namunasi, aralashtirilgan. load_model — asos model 4-bitda, ustiga tez8 ning
adapter i o'qitiladigan holda yuklanadi. Training yozuvi:
yozuv = json.load(open(Path.home() / "tez_runs/tez11/tez11a/output/update_run.json", encoding="utf-8")) for k in ("rows", "replay_rows", "lr", "epochs"): print(f"{k}: {yozuv[k]}")
rows: 4290 replay_rows: 3432 lr: 5e-05 epochs: 1
858 ta yangi qator va 3,432 ta replay — jami 4,290 qator, o'quv tezligi 5e-5. Ikkinchi variant, tez11b, ikki barobar yuqori tezlik bilan o'qitildi.
tez11 natijasi
a, b = hukmlar("tez8g", HAMMASI), hukmlar("tez11ag", HAMMASI) for guruh, toplamlar in GURUHLAR.items(): print(f"{guruh:10s} tez8 {sum(a[k]['correct'] for k in a if k[0] in toplamlar)}, tez11a {sum(b[k]['correct'] for k in b if k[0] in toplamlar)}") ozgardi = sum(a[k]["output"] != b[k]["output"] for k in a) print(f"o'zgargan javoblar: {ozgardi} ({ozgardi / len(a):.1%}), tuzaldi {sum(b[k]['correct'] and not a[k]['correct'] for k in a)}, buzildi {sum(a[k]['correct'] and not b[k]['correct'] for k in a)}") tayyor = [t for t in HAMMASI if (TEZ / f"results/tez11bg_cpu_{t}.jsonl").exists() and sum(1 for _ in open(TEZ / f"results/tez11bg_cpu_{t}.jsonl")) == len([k for k in a if k[0] == t])] a2, b2 = hukmlar("tez8g", tayyor), hukmlar("tez11bg", tayyor) print(f"tez11b ({len(a2)} savolda to'xtatildi): tuzaldi {sum(b2[k]['correct'] and not a2[k]['correct'] for k in a2)}, buzildi {sum(a2[k]['correct'] and not b2[k]['correct'] for k in a2)}")
toza tez8 315, tez11a 310 shovqinli tez8 311, tez11a 310 real tez8 81, tez11a 84 o'zgargan javoblar: 111 (14.9%), tuzaldi 8, buzildi 11 tez11b (520 savolda to'xtatildi): tuzaldi 1, buzildi 9
Davomli training ham javoblarning taxminan yettidan birini o'zgartirdi — noldan trainingdagidan ozgina kam xolos. Real jadvallarda tez11a uchta savolga yaxshiroq, lekin toza va shovqinli to'plamlarda past, va jami bo'yicha tuzatganidan ko'ra ko'proq buzdi. tez11b esa yarmida to'xtatildi: 520 savolning o'zidayoq buzilganlar tuzalganlardan ko'p edi.
Nega aynan shu javoblar o'zgaradi
Bu bobning eng muhim natijasi shu savolga javob. Tez har bir javobning har bir token i uchun modelning ishonchini yozib oladi (29-bobda bu qayerdan kelishini ko'ramiz). tez8 ning shu javoblardagi eng past token ishonchini ikki guruhga ajratamiz: tez11a o'zgartirgan javoblar va o'zgartirmaganlari:
ishonch = {(t, j["id"]): j["min_prob"] for t in HAMMASI for j in map(json.loads, open(TEZ / f"results/tez8c_cpu_{t}.jsonl", encoding="utf-8"))} ozgargan = sorted(ishonch[k] for k in a if a[k]["output"] != b[k]["output"]) qolgan = sorted(ishonch[k] for k in a if a[k]["output"] == b[k]["output"]) for nom, guruh in (("o'zgargan", ozgargan), ("o'zgarmagan", qolgan)): print(f"{nom:12s} {len(guruh):4d} ta: median {guruh[len(guruh) // 2]:.3f}, 0.7 dan past: {sum(p < 0.7 for p in guruh) / len(guruh):.0%}")
o'zgargan 111 ta: median 0.320, 0.7 dan past: 92% o'zgarmagan 634 ta: median 0.985, 0.7 dan past: 7%
Farq keskin. O'zgargan javoblar — tez8 o'zi ikkilangan javoblar: qaysidir token da u ikki variant orasida deyarli teng turgan. O'zgarmaganlari — ishonchli. Ya'ni almashinuv tasodifiy emas: model ikkilangan joylarda har qanday training — noldan yoki davomidan, past yoki yuqori tezlikda — tarozini u yoki bu tomonga og'diradi. Ba'zan to'g'riga, ba'zan noto'g'riga.
Shuning uchun har bir modelda "yangi teshiklar" paydo bo'lardi: tez9, tez10, tez11 — hammasi tez8 ning ikkilangan javoblarini qaytadan "tashlab" ko'rdi. Jurnalda — 24-raqam:
for ustun, matn in zip(("xato", "narxi", "qoida"), jurnal(24)[2:]): print(f"{ustun}: {matn}\n")
xato: Told the user continuation training (tez8 adapter, lr 5e-5, 858 refusal rows plus 3,432 replay rows) was the way to change the model without churn, without a small pilot measuring churn first. narxi: tez11a changed 111 of 745 outputs (15%) and broke 11 tez8 answers for 8 fixes; the changed answers were the ones tez8 was already unsure about (median lowest-token probability 0.32 vs 0.985 for unchanged), so any weight change flips them. qoida: Never promise a training method's side effects before measuring them on a pilot; check output churn on a few hundred questions before a full run.
Qayta training to'xtadi
tez10 va tez11 dan ikki xulosa chiqdi.
Birinchisi — yomon xabar: kichik modelni kichik qadam bilan yaxshilab bo'lmaydi. Har qanday training ikkilangan javoblarni aralashtiradi, va "tez8 ning to'g'ri javoblarini buzmaslik" talabi deyarli bajarib bo'lmaydigan bo'ladi.
Ikkinchisi — yaxshi xabar: model qayerda ikkilanayotganini biz ko'ra olamiz. Ishonch raqami o'zgargan javoblarni oldindan ajratdi. Demak modelni qayta training o'rniga uning atrofida ishlash mumkin: ishonch past bo'lsa — savol berish, natija shubhali bo'lsa — to'xtatish, jadvalni yaxshiroq ta'riflash. Bu qatlamlar modelning og'irliklariga tegmaydi, shuning uchun hech narsani aralashtirmaydi. Ular — 29-bobda.
Xulosa
- tez10 "bitta o'zgarish" bo'lishi kerak edi, lekin toollar fayli o'zgargani uchun har bir qatorning tizim prompti ham o'zgargan: ikki o'zgarish. Trainingdan oldin fayllarni maydonma-maydon solishtirish kerak.
- tez10: jami 745 dan tez8 dan to'qqiz savolga past; real jadvallarda precision va ask recall yuqori.
- 858 ta "qila olmayman" qatori uzun va rasmiy, test savollari esa qisqa: ko'nikma yarim-yarti o'tdi.
- Har bir qayta training javoblarning taxminan oltidan birini almashtiradi (churn), ship chegarasi esa shu almashinuv ichida turardi.
- tez11 (davomli training, replay, past tezlik) ham deyarli shuncha almashtirdi va tuzatganidan ko'p buzdi.
- O'zgaradigan javoblar — tez8 ning o'zi ikkilangan javoblari. Bu ishonchni o'lchab, model atrofida ishlash yo'lini ochdi.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.