🇬🇧 English
A language model is a very large set of numbers, together with a short program that uses those numbers to continue a piece of text. The numbers were set by training: the model was shown a huge amount of text and nudged, again and again, towards predicting each next word a little better. Fine-tuning is the same nudging, continued by you, with examples of your own. The model keeps what it already knows about language, and it learns the one thing your examples show.
The job on this page is a small one that a real business might want. A noodle shop in Chiang Mai takes orders by chat. Customers write things like Two khao soi with chicken, not too spicy, to take away please.
The shop's computer cannot do anything with a sentence. It needs the order in a fixed form that a program can read: {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
Before training, the small model on this page gets 0 of 50 test orders right, and 1 of 50 with the most careful instructions we could write. After 400 examples and a few minutes of training it gets 39 of 50 right. You will also see exactly which 11 it still gets wrong, and why, because that is where the real lesson of fine-tuning is.
On this page you will:
- run the model as it comes, and test it on the job;
- write the training examples with a program;
- look at one example the way the model sees it, as tokens;
- train the model the full way and the LoRA way;
- test both on messages they have never seen;
- use the result inside an ordinary program.
Nothing here was copied from a manual in the hope that it works. Each file was run on a Windows 11 laptop with no graphics card, and what it printed is shown under it. The last part of the page is about something that laptop cannot do: fine-tuning a larger model on a GPU, with the decider library as the example. There the page says plainly what comes from our own runs and what comes from reading the library.
🇹🇭 ไทย
โมเดลภาษาคือชุดตัวเลขขนาดใหญ่มาก กับโปรแกรมสั้น ๆ ที่ใช้ตัวเลขเหล่านั้นเขียนข้อความต่อจากที่ได้รับ ตัวเลขพวกนี้ได้มาจากการฝึก คือให้โมเดลดูข้อความจำนวนมหาศาล แล้วค่อย ๆ ดันมันทีละนิด ครั้งแล้วครั้งเล่า ให้ทายคำถัดไปได้ดีขึ้นอีกหน่อย การปรับจูนก็คือการดันแบบเดียวกันนั้น แต่คุณเป็นคนทำต่อ ด้วยตัวอย่างของคุณเอง โมเดลยังเก็บสิ่งที่มันรู้เรื่องภาษาอยู่แล้วไว้ และได้เรียนสิ่งเดียวที่ตัวอย่างของคุณแสดงให้เห็น
งานในหน้านี้เป็นงานเล็ก ๆ ที่ธุรกิจจริงอาจต้องการ ร้านก๋วยเตี๋ยวแห่งหนึ่งในเชียงใหม่รับออร์เดอร์ทางแชต ลูกค้าพิมพ์มาประมาณว่า Two khao soi with chicken, not too spicy, to take away please.
คอมพิวเตอร์ของร้านเอาประโยคไปทำอะไรต่อไม่ได้ มันต้องการออร์เดอร์ในรูปแบบตายตัวที่โปรแกรมอ่านได้: {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
ก่อนฝึก โมเดลตัวเล็กในหน้านี้ตอบถูก 0 จาก 50 ออร์เดอร์ในชุดทดสอบ และถูก 1 จาก 50 เมื่อใช้คำสั่งที่เราเขียนอย่างละเอียดที่สุดเท่าที่ทำได้ หลังฝึกด้วยตัวอย่าง 400 ชุดเป็นเวลาไม่กี่นาที มันตอบถูก 39 จาก 50 คุณจะได้เห็นด้วยว่า 11 ข้อที่มันยังผิดคือข้อไหน และผิดเพราะอะไร เพราะบทเรียนที่แท้จริงของการปรับจูนอยู่ตรงนั้น
ในหน้านี้คุณจะ:
- รันโมเดลตามสภาพที่มันมา แล้วทดสอบกับงานนี้
- เขียนตัวอย่างสำหรับฝึกด้วยโปรแกรม
- ดูตัวอย่างหนึ่งชุดแบบที่โมเดลเห็น คือเป็นโทเคน
- ฝึกโมเดลทั้งแบบเต็มและแบบ LoRA
- ทดสอบทั้งสองตัวกับข้อความที่มันไม่เคยเห็น
- เอาผลลัพธ์ไปใช้ในโปรแกรมธรรมดา
ไม่มีอะไรในหน้านี้ที่ลอกมาจากคู่มือแล้วหวังว่าจะใช้ได้ ไฟล์ทุกไฟล์ถูกรันจริงบนแล็ปท็อป Windows 11 ที่ไม่มีการ์ดจอ และสิ่งที่มันพิมพ์ออกมาก็แสดงไว้ใต้ไฟล์นั้น ส่วนท้ายของหน้าว่าด้วยสิ่งหนึ่งที่แล็ปท็อปเครื่องนั้นทำไม่ได้ คือการปรับจูนโมเดลที่ใหญ่กว่าบนการ์ดจอ โดยใช้ไลบรารี decider เป็นตัวอย่าง ตรงนั้นหน้านี้จะบอกตรง ๆ ว่าอะไรมาจากการรันของเราเอง และอะไรมาจากการอ่านตัวไลบรารี
🇨🇳 中文
语言模型是一大堆数字,加上一小段程序,这段程序用这些数字把一段文字接着写下去。这些数字是训练出来的:让模型看海量的文字,一次又一次地轻轻推它一下,让它把下一个词猜得更准一点。微调就是同样的“推”,只不过由你接着做,用的是你自己的样例。模型保留它已经懂得的语言知识,同时学会你的样例所展示的那一件事。
这一页的任务很小,却是真实的生意可能需要的。清迈的一家面馆通过聊天接单。顾客会写这样的话:Two khao soi with chicken, not too spicy, to take away please.
店里的电脑拿一句话没办法。它需要订单是一种程序读得懂的固定格式:{"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
训练之前,这一页的小模型在 50 条测试订单里答对 0 条;用上我们能写出的最细致的指令,也只答对 1 条。用 400 个样例训练几分钟之后,它答对 39 条。你还会看到它仍然答错的那 11 条到底是哪些、错在哪里,因为微调真正的道理就在那里。
在这一页里,你会:
- 照原样运行模型,并用这个任务测试它;
- 用程序写出训练样例;
- 按模型看到的样子,也就是 token,看一个样例;
- 用全量的方式和 LoRA 的方式各训练一次模型;
- 用它们都没见过的消息来测试两个模型;
- 在一个普通的程序里使用训练的成果。
这里没有哪一步是从手册上抄来、指望它能用的。每个文件都在一台没有显卡的 Windows 11 笔记本上真正运行过,它打印的内容就显示在文件下面。页面的最后一部分讲那台笔记本做不到的一件事:在 GPU 上微调更大的模型,以 decider 库为例。在那里,页面会明说哪些来自我们自己的运行,哪些来自阅读这个库本身。
should I use LoRA or fine-tuning?as if these were two different things. They are not. Fine-tuning means training a model that already exists on new examples, and there are two ways to do it. Full fine-tuning lets training change every number in the model. LoRA freezes all of those numbers and trains a small set of new ones that sit beside them. Both are fine-tuning; the only question is how many numbers are allowed to move.On this page one file,
train.py, does both. One word on the command line chooses between them, and one short function in the file is the whole difference. Steps 7 to 9 run the two and put the measurements side by side.คนมักถามว่า ควรใช้ LoRA หรือ fine-tuning ดีราวกับว่าเป็นของสองอย่างที่ต่างกัน ซึ่งไม่ใช่ การปรับจูน (fine-tuning) หมายถึงการฝึกโมเดลที่มีอยู่แล้วด้วยตัวอย่างใหม่ และทำได้สองแบบ การปรับจูนแบบเต็ม ยอมให้การฝึกเปลี่ยนตัวเลขทุกตัวในโมเดล ส่วน LoRA ล็อกตัวเลขเหล่านั้นไว้ทั้งหมด แล้วฝึกตัวเลขชุดใหม่ชุดเล็ก ๆ ที่วางอยู่ข้างกัน ทั้งสองแบบคือการปรับจูน คำถามมีข้อเดียวคือยอมให้ตัวเลขขยับได้กี่ตัวในหน้านี้ ไฟล์เดียวคือ
train.py ทำได้ทั้งสองแบบ คำเดียวบนบรรทัดคำสั่งเป็นตัวเลือกว่าจะใช้แบบไหน และฟังก์ชันสั้น ๆ ฟังก์ชันเดียวในไฟล์คือความต่างทั้งหมด ขั้นที่ 7 ถึง 9 รันทั้งสองแบบ แล้ววางผลการวัดไว้เทียบกัน常有人问:我该用 LoRA 还是微调?好像这是两样不同的东西。其实不是。微调的意思是:拿一个已经存在的模型,用新的样例接着训练它,而做法有两种。全量微调允许训练改动模型里的每一个数字。LoRA 把这些数字全部冻结,只训练放在它们旁边的一小组新数字。两种都是微调,唯一的问题是允许多少个数字动。在这一页里,一个文件
train.py 两种都能做。命令行上的一个词决定用哪一种,文件里一个很短的函数就是全部的差别。第 7 到 9 步把两种都跑一遍,再把测量结果并排放在一起。
| Fileไฟล์文件 | What it isคืออะไร它是什么 | Where you use itใช้ตรงไหน用在哪里 |
|---|---|---|
| ask.py | Loads the model and asks it two things.โหลดโมเดลแล้วถามมันสองเรื่อง加载模型,问它两件事。 | Step 2ขั้นที่ 2第 2 步 |
| before.py | Tests the model on the job before any training, with a short instruction and with a long one.ทดสอบโมเดลกับงานนี้ก่อนการฝึกใด ๆ ทั้งด้วยคำสั่งสั้นและคำสั่งยาว在任何训练之前,分别用一条短指令和一条长指令,测试模型做这个任务的表现。 | Step 3ขั้นที่ 3第 3 步 |
| test_handwritten.jsonl | The test that counts: 50 messages written by a person, each with the order it means. Download it.ชุดทดสอบตัวที่ใช้ตัดสิน: ข้อความ 50 ข้อที่คนเขียนเอง แต่ละข้อมีออร์เดอร์ที่มันหมายถึงกำกับไว้ ดาวน์โหลดไฟล์นี้ไว้真正算数的那份测试:50 条由人写的消息,每条都附有它所表示的订单。请下载它。 | Steps 3 and 10ขั้นที่ 3 และ 10第 3 步和第 10 步 |
| make_data.py | Writes the 400 training examples, and a second test of 50.เขียนตัวอย่างสำหรับฝึก 400 ชุด และชุดทดสอบชุดที่สองอีก 50 ข้อ写出 400 个训练样例,以及第二份 50 条的测试。 | Step 4ขั้นที่ 4第 4 步 |
| tokens.py | Shows one example the way the model sees it.แสดงตัวอย่างหนึ่งชุดแบบที่โมเดลเห็น把一个样例按模型看到的样子显示出来。 | Step 5ขั้นที่ 5第 5 步 |
| train.py | Trains the model, the full way or the LoRA way.ฝึกโมเดล แบบเต็มหรือแบบ LoRA训练模型,用全量的方式或 LoRA 的方式。 | Steps 6 to 9ขั้นที่ 6 ถึง 9第 6 到 9 步 |
| score.py | Tests a trained model on both tests.ทดสอบโมเดลที่ฝึกแล้วกับชุดทดสอบทั้งสองชุด用两份测试检验训练好的模型。 | Step 10ขั้นที่ 10第 10 步 |
| use.py | Uses the trained model inside a small program.ใช้โมเดลที่ฝึกแล้วในโปรแกรมเล็ก ๆ在一个小程序里使用训练好的模型。 | Step 11ขั้นที่ 11第 11 步 |
| Wordคำ词 | What it means on this pageความหมายในหน้านี้在这一页里的意思 |
|---|---|
| language model, LLM | A very large set of numbers and the program that uses them to continue a text. LLM is short for large language model.โมเดลภาษา: ชุดตัวเลขขนาดใหญ่มาก กับโปรแกรมที่ใช้ตัวเลขเหล่านั้นเขียนข้อความต่อ LLM ย่อมาจาก large language model คือโมเดลภาษาขนาดใหญ่语言模型:一大堆数字,加上用这些数字把文字接着写下去的程序。LLM 是 large language model(大语言模型)的缩写。 |
| token | A piece of text that the model handles as one unit: a word, a part of a word, or a punctuation mark. The model reads and writes tokens, not letters.โทเคน: ชิ้นของข้อความที่โมเดลจัดการเป็นหนึ่งหน่วย อาจเป็นคำ ส่วนหนึ่งของคำ หรือเครื่องหมายวรรคตอน โมเดลอ่านและเขียนเป็นโทเคน ไม่ใช่ทีละตัวอักษร模型当作一个单位来处理的一小段文字:一个词、词的一部分,或者一个标点。模型读和写的是 token,不是字母。 |
| to train | To change a model's numbers, a little at a time, so that its answers come closer to the examples.ฝึก: เปลี่ยนตัวเลขของโมเดลทีละนิด เพื่อให้คำตอบของมันใกล้กับตัวอย่างมากขึ้น训练:一点一点地改动模型的数字,让它的回答更接近样例。 |
| fine-tuning | Training a model that has already been trained, on new examples, to teach it one job.การปรับจูน: การฝึกโมเดลที่ฝึกมาแล้วด้วยตัวอย่างใหม่ เพื่อสอนงานหนึ่งอย่างให้มัน微调:用新的样例训练一个已经训练过的模型,教会它一件事。 |
| full fine-tuning | Fine-tuning in which every number in the model may change.การปรับจูนแบบเต็ม: การปรับจูนที่ตัวเลขทุกตัวในโมเดลเปลี่ยนได้全量微调:模型里的每个数字都可以改动的微调。 |
| LoRA | Fine-tuning in which the model's numbers are frozen, and a small set of new numbers placed beside them is trained. Short for low-rank adaptation.การปรับจูนที่ล็อกตัวเลขของโมเดลไว้ แล้วฝึกตัวเลขชุดใหม่ชุดเล็ก ๆ ที่วางไว้ข้างกัน ย่อมาจาก low-rank adaptation一种微调:模型的数字被冻结,只训练放在它们旁边的一小组新数字。它是 low-rank adaptation(低秩适配)的缩写。 |
| adapter | The small set of new numbers that LoRA trains. This page also calls it the add-on. It is saved as a small file of its own.ตัวเลขชุดใหม่ชุดเล็ก ๆ ที่ LoRA ฝึก หน้านี้เรียกมันว่าตัวเสริมด้วย มันถูกบันทึกเป็นไฟล์เล็ก ๆ ของตัวเอง适配器:LoRA 训练的那一小组新数字。这一页也把它叫作附加模块。它单独存成一个小文件。 |
| loss | One number that says how wrong the model was on an example. Training tries to make it smaller.ตัวเลขหนึ่งตัวที่บอกว่าโมเดลผิดไปมากแค่ไหนกับตัวอย่างหนึ่งชุด การฝึกพยายามทำให้มันเล็กลง损失:一个数字,表示模型在一个样例上错得有多厉害。训练就是要让它变小。 |
| step | One round of training: the model sees an example, its loss is measured, and its numbers are moved a little.สเต็ป: การฝึกหนึ่งรอบ โมเดลเห็นตัวอย่างหนึ่งชุด วัดค่า loss แล้วขยับตัวเลขของมันนิดหนึ่ง训练步:训练的一轮。模型看一个样例,量出它的损失,再把它的数字挪动一点。 |
| learning rate | How far one step may move a number.ระยะที่หนึ่งสเต็ปขยับตัวเลขหนึ่งตัวได้学习率:一步最多可以把一个数字挪动多远。 |
| prompt | The text you give a model to tell it what to do. This page also calls it the instruction.พรอมต์: ข้อความที่คุณให้โมเดลเพื่อบอกว่าต้องทำอะไร หน้านี้เรียกมันว่าคำสั่งด้วย提示词:你交给模型、告诉它该做什么的文字。这一页也把它叫作指令。 |
| JSON | A way of writing data as text that programs can read, with names and values inside curly brackets.วิธีเขียนข้อมูลเป็นข้อความที่โปรแกรมอ่านได้ โดยมีชื่อและค่าอยู่ในวงเล็บปีกกา一种把数据写成文字的方式,程序能读懂,名称和值写在花括号里。 |
| test | Examples that the model never trains on. They are kept back to find out whether it really learned the job.ชุดทดสอบ: ตัวอย่างที่โมเดลไม่เคยใช้ฝึก เก็บแยกไว้เพื่อดูว่ามันเรียนรู้งานได้จริงหรือไม่测试:模型从不拿来训练的样例。留着它们,是为了弄清模型是不是真的学会了这件事。 |
| GPU | A graphics card. It does the arithmetic of training many times faster than a computer's ordinary processor.การ์ดจอ มันคำนวณงานของการฝึกได้เร็วกว่าโปรเซสเซอร์ธรรมดาของคอมพิวเตอร์หลายเท่า显卡。训练里的那些运算,它做起来比电脑的普通处理器快很多倍。 |
🧩 Tutorial 1 — The model, before any training · บทที่ 1 — โมเดล ก่อนการฝึกใด ๆ · 教程 1 —— 训练之前的模型
🇬🇧 English
Before you change a model, find out what it does as it comes. There are two reasons. You may discover that it already does the job, and then there is nothing to train. And if it does not, you need a score from before, or you will never know how much the training helped.
🇹🇭 ไทย
ก่อนจะเปลี่ยนโมเดล ให้ดูก่อนว่ามันทำอะไรได้ตามสภาพที่มันมา มีเหตุผลสองข้อ ข้อแรก คุณอาจพบว่ามันทำงานนี้ได้อยู่แล้ว ถ้าเป็นอย่างนั้นก็ไม่มีอะไรต้องฝึก ข้อที่สอง ถ้ามันทำไม่ได้ คุณก็ต้องมีคะแนนจากก่อนฝึกเก็บไว้ ไม่อย่างนั้นจะไม่มีทางรู้เลยว่าการฝึกช่วยได้มากแค่ไหน
🇨🇳 中文
改动一个模型之前,先弄清它原本能做什么。原因有两个。你可能发现它本来就能做这件事,那就没什么可训练的了。如果它做不了,你也需要一个训练之前的分数,不然永远不知道训练到底帮了多少。
1 A separate Python, and three libraries · Python อีกชุดหนึ่ง กับไลบรารีสามตัว · 一份单独的 Python,和三个库
🇬🇧 English
Make a new folder for this page and open a terminal in it. The first command below makes a virtual environment, which is a separate copy of Python with its own libraries, kept in a folder named venv. The second installs three libraries into it.
torch is PyTorch, the library that does the arithmetic: it holds the model's numbers and knows how to change them. transformers, from Hugging Face, knows how to download a model, how to turn text into tokens and back, and how to run the model. peft, also from Hugging Face, adds LoRA. Its name is short for parameter-efficient fine-tuning, which means fine-tuning that trains only a few of the numbers.
Every command on this page names the Python inside venv, so you can always see which Python is doing the work. On a Mac, write venv/bin/python wherever this page writes venv\Scripts\python.
🇹🇭 ไทย
สร้างโฟลเดอร์ใหม่สำหรับหน้านี้ แล้วเปิดเทอร์มินัลในโฟลเดอร์นั้น คำสั่งแรกข้างล่างสร้างสภาพแวดล้อมเสมือน ซึ่งก็คือ Python อีกชุดที่แยกออกมา มีไลบรารีเป็นของตัวเอง เก็บอยู่ในโฟลเดอร์ชื่อ venv คำสั่งที่สองติดตั้งไลบรารีสามตัวลงไปในนั้น
torch คือ PyTorch ไลบรารีที่ทำหน้าที่คำนวณ มันเก็บตัวเลขของโมเดลและรู้วิธีเปลี่ยนตัวเลขเหล่านั้น transformers จาก Hugging Face รู้วิธีดาวน์โหลดโมเดล วิธีแปลงข้อความเป็นโทเคนและแปลงกลับ และวิธีรันโมเดล peft ซึ่งมาจาก Hugging Face เช่นกัน เพิ่ม LoRA เข้ามา ชื่อของมันย่อมาจาก parameter-efficient fine-tuning หมายถึงการปรับจูนที่ฝึกตัวเลขเพียงไม่กี่ตัว
ทุกคำสั่งในหน้านี้ระบุ Python ตัวที่อยู่ใน venv คุณจึงมองเห็นได้เสมอว่า Python ชุดไหนกำลังทำงาน บนแมค ให้พิมพ์ venv/bin/python ในทุกที่ที่หน้านี้เขียนว่า venv\Scripts\python
🇨🇳 中文
为这一页新建一个文件夹,并在里面打开终端。下面第一条命令创建一个虚拟环境,也就是一份单独的 Python,有自己的一套库,放在名叫 venv 的文件夹里。第二条命令往里面安装三个库。
torch 就是 PyTorch,负责运算的库:它保存模型的数字,也知道怎样改动它们。transformers 来自 Hugging Face,它会下载模型,会把文字变成 token 再变回来,也会运行模型。peft 同样来自 Hugging Face,它提供 LoRA。这个名字是 parameter-efficient fine-tuning 的缩写,意思是只训练少数几个数字的微调。
这一页的每条命令都写明用 venv 里的那个 Python,所以你随时看得出是哪一份 Python 在干活。在 Mac 上,凡是这一页写 venv\Scripts\python 的地方,都要写成 venv/bin/python。
🪟 WINDOWS
python -m venv venv
venv\Scripts\python -m pip install torch transformers peft
🍎 MAC · not run by us · เราไม่ได้รันเอง · 我们没有运行过
python3 -m venv venv
venv/bin/python -m pip install torch transformers peft
venv of 861.5 MB. Ours held torch 2.14.1, transformers 5.19.0 and peft 0.21.2, and every result on this page comes from those versions.โฟลเดอร์ชื่อ venv ขนาด 861.5 MB ของเรามี torch 2.14.1, transformers 5.19.0 และ peft 0.21.2 และผลลัพธ์ทุกอย่างในหน้านี้ได้จากเวอร์ชันเหล่านี้一个名叫 venv 的文件夹,861.5 MB。我们的环境里是 torch 2.14.1、transformers 5.19.0 和 peft 0.21.2,这一页的所有结果都出自这几个版本。
pip install torch brings a version of about 125 MB that works on the ordinary processor. On Linux the same command brings the version for NVIDIA graphics cards: a file of about 555 MB, and NVIDIA's own libraries on top of it. If your Linux computer has no such card, PyTorch keeps the small versions at a separate address, and you name it with --index-url https://download.pytorch.org/whl/cpu when you install torch.The sizes are the ones the Python Package Index lists for torch 2.14.1, and the address is PyTorch's own. We ran nothing on Linux.บน Windows และบนแมค pip install torch จะได้เวอร์ชันขนาดราว 125 MB ที่ทำงานบนโปรเซสเซอร์ธรรมดา บน Linux คำสั่งเดียวกันจะได้เวอร์ชันสำหรับการ์ดจอ NVIDIA เป็นไฟล์ขนาดราว 555 MB แล้วยังมีไลบรารีของ NVIDIA เองตามมาอีก ถ้าคอมพิวเตอร์ Linux ของคุณไม่มีการ์ดจอแบบนั้น PyTorch เก็บเวอร์ชันเล็กไว้อีกที่อยู่หนึ่ง ให้ระบุที่อยู่นั้นด้วย --index-url https://download.pytorch.org/whl/cpu ตอนติดตั้ง torchขนาดไฟล์เป็นตัวเลขที่ Python Package Index แสดงไว้สำหรับ torch 2.14.1 และที่อยู่นี้เป็นของ PyTorch เอง เราไม่ได้รันอะไรบน Linux เลย在 Windows 和 Mac 上,pip install torch 装来的是一个约 125 MB、在普通处理器上运行的版本。在 Linux 上,同一条命令装来的是给 NVIDIA 显卡用的版本:一个约 555 MB 的文件,外加 NVIDIA 自己的一批库。如果你的 Linux 电脑没有这种显卡,PyTorch 把小的版本放在另一个地址,安装 torch 时用 --index-url https://download.pytorch.org/whl/cpu 指明它就行。这些大小是 Python Package Index 为 torch 2.14.1 列出的数字,地址是 PyTorch 自己的。我们没有在 Linux 上运行过任何东西。
2 Load the model and ask it something · โหลดโมเดลแล้วลองถาม · 加载模型,问它点什么
🇬🇧 English
The model is SmolLM2-135M-Instruct, made by Hugging Face. It has 134,515,008 numbers. That is tiny for a language model: decider, the model in our pond game, has fourteen times as many, and the largest models you can download have thousands of times as many. Tiny is the point here. It runs on a laptop, and it can be trained on one in minutes. Its licence, Apache-2.0, lets you use it and change it freely.
ask.py does three things that every file on this page repeats. AutoTokenizer.from_pretrained loads the tokenizer, which turns text into tokens and tokens back into text. AutoModelForCausalLM.from_pretrained loads the model itself; the first time, it downloads 272.4 MB and keeps them in a cache folder. And reply_to sends a chat to the model and returns what it writes back.
A chat is a list of dictionaries. Each one has a role, which says who is speaking, and a content, which is what they say. The role system is the instruction to the model, user is the person, and assistant is the model. The model itself knows nothing about lists and dictionaries. It only continues text. So apply_chat_template writes the chat out as one piece of text, with a marker around each turn, in exactly the form this model was trained on. add_generation_prompt=True ends that text with the marker that means now the assistant speaks
, and the model carries on from there.
🇹🇭 ไทย
โมเดลตัวนี้คือ SmolLM2-135M-Instruct ของ Hugging Face มันมีตัวเลข 134,515,008 ตัว ซึ่งถือว่าเล็กจิ๋วสำหรับโมเดลภาษา decider โมเดลในเกมบ่อปลาของเรามีมากกว่านี้สิบสี่เท่า และโมเดลตัวใหญ่ที่สุดที่ดาวน์โหลดได้มีมากกว่านี้หลายพันเท่า แต่ความเล็กคือจุดประสงค์ของหน้านี้ มันรันบนแล็ปท็อปได้ และฝึกบนแล็ปท็อปได้ในไม่กี่นาที สัญญาอนุญาตของมันคือ Apache-2.0 ซึ่งให้คุณใช้และแก้ไขได้อย่างเสรี
ask.py ทำสามอย่างที่ทุกไฟล์ในหน้านี้ทำซ้ำ AutoTokenizer.from_pretrained โหลด tokenizer ซึ่งแปลงข้อความเป็นโทเคน และแปลงโทเคนกลับเป็นข้อความ AutoModelForCausalLM.from_pretrained โหลดตัวโมเดล ครั้งแรกมันจะดาวน์โหลด 272.4 MB แล้วเก็บไว้ในโฟลเดอร์แคช ส่วน reply_to ส่งบทสนทนาให้โมเดล แล้วคืนสิ่งที่โมเดลเขียนตอบกลับมา
บทสนทนาคือลิสต์ของดิกชันนารี แต่ละตัวมี role ที่บอกว่าใครกำลังพูด และ content คือสิ่งที่พูด role system คือคำสั่งถึงโมเดล user คือคน และ assistant คือโมเดล ตัวโมเดลเองไม่รู้จักลิสต์หรือดิกชันนารี มันทำได้แค่เขียนข้อความต่อ apply_chat_template จึงเขียนบทสนทนาออกมาเป็นข้อความชิ้นเดียว มีเครื่องหมายกำกับรอบแต่ละตา ในรูปแบบเดียวกับที่โมเดลตัวนี้ถูกฝึกมาทุกประการ add_generation_prompt=True ปิดท้ายข้อความนั้นด้วยเครื่องหมายที่หมายความว่า ถึงตา assistant พูดแล้ว
และโมเดลก็เขียนต่อจากตรงนั้น
🇨🇳 中文
这个模型是 Hugging Face 出品的 SmolLM2-135M-Instruct。它有 134,515,008 个数字。对语言模型来说,这非常小:我们池塘游戏里的模型 decider,数字是它的十四倍;能下载到的最大的模型,是它的几千倍。但这里要的就是小。它能在笔记本上运行,也能在笔记本上几分钟就训练完。它的许可证是 Apache-2.0,允许你自由使用和修改。
ask.py 做了三件事,这一页的每个文件都会重复这三件事。AutoTokenizer.from_pretrained 加载分词器(tokenizer),它把文字变成 token,再把 token 变回文字。AutoModelForCausalLM.from_pretrained 加载模型本身;第一次运行时,它会下载 272.4 MB,存进一个缓存文件夹。reply_to 则把一段对话交给模型,并返回模型写回来的内容。
一段对话是一个由字典组成的列表。每个字典有一个 role,说明是谁在说话,还有一个 content,就是说的内容。system 这个角色是给模型的指令,user 是人,assistant 是模型。模型自己并不懂什么列表和字典,它只会把文字接着写下去。所以 apply_chat_template 把对话写成一整段文字,每一轮前后都有标记,格式和这个模型训练时用的一模一样。add_generation_prompt=True 让这段文字以一个表示现在轮到 assistant 说话
的标记结尾,模型就从那里接着写。
💻 RUN IT
venv\Scripts\python ask.py
📄 NEW FILE · ask.py
"""ask.py - load a small language model and ask it two things.
python ask.py
The first run downloads the model, about 270 MB. Later runs start in seconds.
"""
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
def reply_to(model, tokenizer, chat):
"""Send a chat to the model and return the text it writes back."""
chat_tokens = tokenizer.apply_chat_template(
chat, add_generation_prompt=True, return_tensors="pt")
question_tokens = chat_tokens["input_ids"]
# do_sample=False always takes the most likely token: the same reply every run.
output = model.generate(question_tokens, max_new_tokens=60, do_sample=False)
# output is one row of tokens: first the question, then the reply.
# shape[1] is how many tokens the question has, so this keeps the reply.
reply_tokens = output[0][question_tokens.shape[1]:]
return tokenizer.decode(reply_tokens, skip_special_tokens=True)
def main():
started = time.perf_counter()
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32)
print(f"Loaded in {time.perf_counter() - started:.1f} seconds.")
print()
chat = [{"role": "user", "content": "What is the capital of Thailand?"}]
print("What the model reads:")
print(tokenizer.apply_chat_template(
chat, tokenize=False, add_generation_prompt=True))
print("What it writes back:")
print(reply_to(model, tokenizer, chat))
print()
chat = [
{"role": "system",
"content": "You take orders for a noodle shop. Reply with the order as JSON."},
{"role": "user",
"content": "Two khao soi with chicken, not too spicy, to take away please."},
]
print("The order, before any training:")
print(reply_to(model, tokenizer, chat))
if __name__ == "__main__":
main()
🖨️ WHAT IT PRINTED
Loaded in 2.0 seconds.
What the model reads:
<|im_start|>system
You are a helpful AI assistant named SmolLM, trained by Hugging Face<|im_end|>
<|im_start|>user
What is the capital of Thailand?<|im_end|>
<|im_start|>assistant
What it writes back:
The capital of Thailand is Bangkok.
The order, before any training:
{
"name": "Khao Soi with Chicken",
"quantity": 2,
"price": 10.99,
"quantity_per_person": 2,
"price_per_person": 10.99,
"quantity
🇬🇧 English
Look at what the model read. We sent one line, and the text has a first turn that we never wrote: You are a helpful AI assistant named SmolLM, trained by Hugging Face. The template adds that instruction by itself when a chat has none. It is a good reminder that the template, and not you, decides the exact text the model sees.
The model's answer to the first question is right. Its answer to the order is not. It did write something shaped like JSON, because it has seen a great deal of JSON, but it invented the names, and it invented a price. Nothing in the message told it which form the shop wants. It also did not stop: it was still writing when it reached the limit of 60 tokens that max_new_tokens sets.
🇹🇭 ไทย
ดูสิ่งที่โมเดลอ่าน เราส่งไปบรรทัดเดียว แต่ข้อความกลับมีตาแรกที่เราไม่เคยเขียน คือ You are a helpful AI assistant named SmolLM, trained by Hugging Face เทมเพลตใส่คำสั่งนี้เองเมื่อบทสนทนาไม่มีคำสั่ง นี่เป็นเครื่องเตือนที่ดีว่า เทมเพลตต่างหาก ไม่ใช่คุณ ที่เป็นตัวกำหนดข้อความที่โมเดลเห็นจริง ๆ
คำตอบของโมเดลสำหรับคำถามแรกถูก ส่วนคำตอบสำหรับออร์เดอร์นั้นไม่ถูก มันเขียนอะไรบางอย่างที่มีรูปร่างเหมือน JSON ออกมาจริง เพราะมันเคยเห็น JSON มามาก แต่มันแต่งชื่อขึ้นเอง และแต่งราคาขึ้นเองด้วย ไม่มีอะไรในข้อความที่บอกมันว่าร้านต้องการรูปแบบไหน แถมมันยังไม่หยุด มันยังเขียนอยู่ตอนที่ชนเพดาน 60 โทเคนที่ max_new_tokens กำหนดไว้
🇨🇳 中文
看看模型读到的内容。我们只发了一行,文字里却多出了一轮我们从没写过的话:You are a helpful AI assistant named SmolLM, trained by Hugging Face。对话里没有指令时,模板会自己加上这一条。这是个很好的提醒:决定模型到底看到什么文字的,是模板,不是你。
模型对第一个问题的回答是对的。对那份订单的回答则不对。它确实写出了一段样子像 JSON 的东西,因为它见过大量的 JSON;但名称是它编的,价格也是它编的。消息里没有任何东西告诉它店里要的是哪种格式。它也没有停下来:写到 max_new_tokens 规定的 60 个 token 上限时,它还在写。
sending unauthenticated requests to the HF Hub. The model is public, so you need no account. On Windows it also warns that the cache
uses symlinks by defaultand that your computer does not support them. The files are simply stored another way, and everything works.
dtype=torch.float32 is the setting to keep. The model's file stores every number in 16 bits, and transformers loads them just as they are. For replies that makes little difference. For training it is ruinous on a laptop's processor: with the 16-bit numbers, one step of our training took 113 times as long. So every file on this page asks for 32 bits.การรันครั้งแรกจะพิมพ์คำเตือนว่าคุณกำลัง sending unauthenticated requests to the HF Hubโมเดลนี้เปิดสาธารณะ คุณจึงไม่ต้องมีบัญชี บน Windows มันยังเตือนด้วยว่าแคช
uses symlinks by defaultและเครื่องของคุณไม่รองรับ ไฟล์แค่ถูกเก็บด้วยอีกวิธีหนึ่ง และทุกอย่างใช้งานได้
dtype=torch.float32 คือการตั้งค่าที่ควรคงไว้ ไฟล์ของโมเดลเก็บตัวเลขแต่ละตัวไว้ 16 บิต และ transformers โหลดมันมาตามนั้น สำหรับการตอบ เรื่องนี้แทบไม่ต่างกัน แต่สำหรับการฝึก มันช้าจนใช้ไม่ได้บนโปรเซสเซอร์ของแล็ปท็อป เมื่อใช้ตัวเลข 16 บิต การฝึกของเราหนึ่งสเต็ปใช้เวลานานขึ้น 113 เท่า ทุกไฟล์ในหน้านี้จึงขอตัวเลข 32 บิต第一次运行会打印一条警告,说你在 sending unauthenticated requests to the HF Hub。这个模型是公开的,所以不需要账号。在 Windows 上,它还会警告说缓存
uses symlinks by default,而你的电脑不支持。文件只是换了一种方式存放,一切照常工作。
dtype=torch.float32 是要保留的设置。模型的文件里,每个数字用 16 位存储,transformers 会原样加载它们。对回复来说,这几乎没有差别。对训练来说,在笔记本的处理器上它慢得没法用:用 16 位的数字时,我们的训练一步花的时间是原来的 113 倍。所以这一页的每个文件都要求用 32 位。
3 Give it the job, and try a prompt first · ให้งานมันทำ และลองใช้พรอมต์ก่อน · 把任务交给它,先试试提示词
🇬🇧 English
Fine-tuning costs time and care, and there is a much cheaper thing to try first: tell the model exactly what you want. A long, careful instruction with a few solved examples in it is often enough, and when it is, you are finished. So before.py tries it both ways, and counts.
It uses test_handwritten.jsonl, a file of 50 messages that a person wrote, each with the order it means. Download it into your folder. One line of the file is one example. The model will never be trained on these messages. They exist only for testing.
INSTRUCTION is the one-line instruction from step 2. LONG_INSTRUCTION spells out all five names, every allowed answer, and what to write when the customer does not say. long_chat then adds the three orders in SOLVED_EXAMPLES, already answered, as if the conversation had been going on for a while, before the real message. Showing solved examples inside the prompt like this is called few-shot prompting.
is_right is the judge. json.loads turns the reply into a Python dictionary, and == compares it with the order we wanted, so a reply is right only when all five answers are right. If the reply is not JSON at all, json.loads raises an error, except catches it, and the reply counts as wrong.
🇹🇭 ไทย
การปรับจูนต้องใช้ทั้งเวลาและความใส่ใจ และมีสิ่งที่ถูกกว่ามากให้ลองก่อน คือบอกโมเดลให้ชัดว่าคุณต้องการอะไร คำสั่งที่ยาวและละเอียด พร้อมตัวอย่างที่เฉลยไว้แล้วสองสามชุด มักจะเพียงพอ และถ้าเพียงพอ คุณก็เสร็จงานแล้ว before.py จึงลองทั้งสองแบบ แล้วนับผล
มันใช้ test_handwritten.jsonl ไฟล์ที่มีข้อความ 50 ข้อที่คนเขียนเอง แต่ละข้อมีออร์เดอร์ที่มันหมายถึงกำกับไว้ ดาวน์โหลดไฟล์นี้มาไว้ในโฟลเดอร์ของคุณ หนึ่งบรรทัดในไฟล์คือหนึ่งตัวอย่าง โมเดลจะไม่ถูกฝึกด้วยข้อความเหล่านี้เลย มันมีไว้เพื่อทดสอบเท่านั้น
INSTRUCTION คือคำสั่งบรรทัดเดียวจากขั้นที่ 2 ส่วน LONG_INSTRUCTION เขียนไว้ครบทั้งชื่อห้าชื่อ คำตอบที่อนุญาตทุกค่า และสิ่งที่ต้องเขียนเมื่อลูกค้าไม่ได้บอก จากนั้น long_chat ใส่ออร์เดอร์สามรายการใน SOLVED_EXAMPLES ที่ตอบไว้แล้วเข้าไปก่อนข้อความจริง ราวกับว่าบทสนทนาดำเนินมาได้สักพักแล้ว การแสดงตัวอย่างที่เฉลยแล้วไว้ในพรอมต์แบบนี้เรียกว่า few-shot prompting
is_right คือผู้ตัดสิน json.loads แปลงคำตอบให้เป็นดิกชันนารีของ Python และ == เทียบมันกับออร์เดอร์ที่เราต้องการ คำตอบจึงถือว่าถูกก็ต่อเมื่อถูกครบทั้งห้าค่า ถ้าคำตอบไม่ใช่ JSON เลย json.loads จะแจ้งข้อผิดพลาด except จับมันไว้ และคำตอบนั้นนับว่าผิด
🇨🇳 中文
微调要花时间,也要花心思,而有一件便宜得多的事可以先试:把你要什么清清楚楚地告诉模型。一条又长又细致的指令,里面再放几个做好的例子,往往就够了;够了的话,你的事也就办完了。所以 before.py 两种都试,然后数一数结果。
它用的是 test_handwritten.jsonl,一个有 50 条消息的文件,都是人写的,每条都附有它所表示的订单。把它下载到你的文件夹里。文件的一行就是一个样例。模型永远不会用这些消息来训练,它们只用来测试。
INSTRUCTION 是第 2 步里那条只有一行的指令。LONG_INSTRUCTION 把五个名称、每一个允许的答案,以及顾客没说时该写什么,全都写了出来。接着,long_chat 在真正的消息之前,先放进 SOLVED_EXAMPLES 里那三份已经答好的订单,就好像这场对话已经进行了一会儿。像这样在提示词里展示做好的例子,叫作 few-shot prompting(少样本提示)。
is_right 是裁判。json.loads 把回复变成一个 Python 字典,== 再把它和我们想要的订单比较,所以五个答案全对,回复才算对。如果回复根本不是 JSON,json.loads 会报错,except 接住这个错误,这条回复就算错。
💻 RUN IT
venv\Scripts\python before.py
📄 NEW FILE · before.py
"""before.py - test the model on the job before any training.
python before.py
It sends the 50 messages in test_handwritten.jsonl to the model twice: first
with a short instruction, then with a long one that states every rule and shows
three solved examples. Each time it counts the orders the model gets right.
"""
import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
INSTRUCTION = "You take orders for a noodle shop. Reply with the order as JSON."
LONG_INSTRUCTION = """You take orders for a noodle shop.
Reply with the order as JSON, and with nothing else.
The JSON has exactly these five keys:
"dish": "khao soi", "nam ngiao", "pad thai" or "pad see ew"
"protein": "chicken", "pork", "beef" or "tofu"
"spice": "none", "mild", "medium" or "hot". Use "medium" if the customer does not say.
"count": how many, as a number. Use 1 if the customer does not say.
"takeaway": true or false. Use false if the customer does not say."""
SOLVED_EXAMPLES = [
{"message": "Three khao soi with pork, a little spicy, to go please.",
"order": {"dish": "khao soi", "protein": "pork", "spice": "mild",
"count": 3, "takeaway": True}},
{"message": "pad thai tofu no chilli",
"order": {"dish": "pad thai", "protein": "tofu", "spice": "none",
"count": 1, "takeaway": False}},
{"message": "Hello, two nam ngiao with beef, very spicy, eating here.",
"order": {"dish": "nam ngiao", "protein": "beef", "spice": "hot",
"count": 2, "takeaway": False}},
]
def read_examples(name):
"""Every line of the file is one example, written as JSON."""
examples = []
with open(name, encoding="utf-8") as file:
for line in file:
examples.append(json.loads(line))
return examples
def short_chat(message):
"""The one-line instruction, then the message."""
return [
{"role": "system", "content": INSTRUCTION},
{"role": "user", "content": message},
]
def long_chat(message):
"""The rules, then three orders already answered, then the new message."""
chat = [{"role": "system", "content": LONG_INSTRUCTION}]
for solved in SOLVED_EXAMPLES:
chat.append({"role": "user", "content": solved["message"]})
chat.append({"role": "assistant", "content": json.dumps(solved["order"])})
chat.append({"role": "user", "content": message})
return chat
def reply_to(model, tokenizer, chat):
"""The model's reply to a chat, on one line."""
chat_tokens = tokenizer.apply_chat_template(
chat, add_generation_prompt=True, return_tensors="pt")
question_tokens = chat_tokens["input_ids"]
output = model.generate(question_tokens, max_new_tokens=50, do_sample=False)
# output is the question and then the reply. Keep the reply.
reply_tokens = output[0][question_tokens.shape[1]:]
reply = tokenizer.decode(reply_tokens, skip_special_tokens=True)
return " ".join(reply.split())
def is_right(reply, order):
"""True when the reply is JSON and says exactly what the order says."""
try:
return json.loads(reply) == order
except ValueError: # json.loads could not read the reply as JSON
return False
def main():
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32)
tests = read_examples("test_handwritten.jsonl")
for instruction in ["short", "long"]:
right = 0
shown = 0
for test in tests:
if instruction == "short":
chat = short_chat(test["message"])
else:
chat = long_chat(test["message"])
reply = reply_to(model, tokenizer, chat)
if is_right(reply, test["order"]):
right = right + 1
if shown < 2: # show the first two replies as they are
print(test["message"])
print(" ", reply)
shown = shown + 1
print(f"{instruction} instruction: {right} of {len(tests)} right")
print()
if __name__ == "__main__":
main()
🖨️ WHAT IT PRINTED
Two khao soi with chicken, mild, to take away please.
{ "name": "Khao Soi with Chicken", "quantity": 2, "price": 10.99, "quantity": 2, "price": 10.99,
One pad thai with tofu, not spicy. I will eat here.
{ "order": { "name": "One Pad Thai with Tofu", "quantity": 2, "price": 10.99, "menu": [ { "name": "Pad
short instruction: 0 of 50 right
Two khao soi with chicken, mild, to take away please.
{"dish": "khao soi", "protein": "chicken", "spice": "chicken", "count": 1, "takeaway": false}
One pad thai with tofu, not spicy. I will eat here.
{"dish": "pad thai", "protein": "beef", "spice": "hot", "count": 1, "takeaway": false}
long instruction: 1 of 50 right
🇬🇧 English
With the short instruction the model gets 0 of 50. With the long one it gets 1. But look at how its replies changed. With the long instruction it writes exactly the five names the shop wants, in the right order. What it puts after them is still wrong: "spice": "chicken", and beef for a customer who asked for tofu. A model this small can copy the shape of an example it has just been shown. It cannot follow eight lines of rules.
A much larger model would very likely do this job from the long instruction alone, and if you have one, try that before you train anything. We did not measure one here. The reasons to fine-tune a small model anyway are that it runs on a cheap computer, it answers fast, and the customers' messages never leave the shop.
🇹🇭 ไทย
ด้วยคำสั่งสั้น โมเดลตอบถูก 0 จาก 50 ด้วยคำสั่งยาว มันตอบถูก 1 แต่ลองดูว่าคำตอบของมันเปลี่ยนไปอย่างไร ด้วยคำสั่งยาว มันเขียนชื่อทั้งห้าชื่อที่ร้านต้องการได้ตรงเป๊ะ และเรียงถูกลำดับ สิ่งที่มันใส่ไว้หลังชื่อเหล่านั้นยังผิดอยู่: "spice": "chicken" และ beef ให้ลูกค้าที่สั่งเต้าหู้ โมเดลขนาดเล็กเท่านี้ลอกรูปร่างของตัวอย่างที่เพิ่งได้เห็นได้ แต่มันทำตามกฎแปดบรรทัดไม่ได้
โมเดลที่ใหญ่กว่านี้มากน่าจะทำงานนี้ได้จากคำสั่งยาวเพียงอย่างเดียว และถ้าคุณมีโมเดลแบบนั้น ให้ลองวิธีนั้นก่อนจะฝึกอะไร เราไม่ได้วัดโมเดลแบบนั้นในหน้านี้ เหตุผลที่ยังควรปรับจูนโมเดลตัวเล็กก็คือ มันรันบนคอมพิวเตอร์ราคาถูกได้ ตอบได้เร็ว และข้อความของลูกค้าไม่ต้องออกไปนอกร้านเลย
🇨🇳 中文
用短指令,模型在 50 条里答对 0 条。用长指令,答对 1 条。但看看它的回复变成了什么样。用了长指令,它写出的正是店里要的那五个名称,顺序也对。只是名称后面填的内容还是错的:"spice": "chicken",还给一位点豆腐的顾客写了 beef。这么小的模型,能照着刚看到的例子把样子抄下来,却没法遵守八行规则。
一个大得多的模型,很可能只靠这条长指令就能做好这件事;如果你手头有,训练任何东西之前先试试它。我们在这里没有测过这样的模型。即便如此,仍然值得微调一个小模型,理由是:它能在便宜的电脑上运行,回答得快,而且顾客的消息永远不用离开这家店。
📝 Tutorial 2 — The examples · บทที่ 2 — ตัวอย่าง · 教程 2 —— 样例
🇬🇧 English
A model learns from examples and from nothing else. It never sees what you meant, only what you showed it. So most of the real work in fine-tuning is making the examples, and most of what goes wrong later can be traced back to them.
🇹🇭 ไทย
โมเดลเรียนรู้จากตัวอย่าง และไม่ได้เรียนจากอย่างอื่นเลย มันไม่เคยเห็นสิ่งที่คุณตั้งใจ มันเห็นแต่สิ่งที่คุณแสดงให้ดู งานจริงส่วนใหญ่ของการปรับจูนจึงเป็นการทำตัวอย่าง และสิ่งที่ผิดพลาดในภายหลังส่วนใหญ่ก็สืบย้อนกลับมาที่ตัวอย่างได้
🇨🇳 中文
模型只从样例里学习,别的什么都不看。它从来看不到你的本意,只看得到你给它看的东西。所以微调里真正的功夫,大部分花在做样例上;后来出的问题,大部分也能追溯到样例上。
4 Write 400 examples with a program · เขียนตัวอย่าง 400 ชุดด้วยโปรแกรม · 用程序写出 400 个样例
🇬🇧 English
An example has two parts: a message, and the order that the message means. You could write four hundred of them by hand. A program is quicker, and it can never pair a message with the wrong order, because it starts from the order and then writes a message for it.
random_order picks the five answers at random. message_for then writes one way a customer might say them. It picks a word for the number, one of three ways to name the food, a phrase for the spice and a phrase for the place, an opening and an ending, and it puts the spice and the place in a random order. Now and then it writes the whole message without capital letters, as people do.
Three rules are hidden in the word lists, and they matter. One of the phrases for medium is the empty text "", which means the customer said nothing about spice. One of the phrases for eating at the shop is empty too. And a is one of the words for the number 1. So the examples themselves carry the shop's habits: no word about spice means medium, and no word about the place means the customer eats here. Nobody will ever state these rules to the model. It has to find them in the examples.
The program writes 450 different messages. The first 400 go into train.jsonl, and the model will learn from those. The other 50 go into test_generated.jsonl and are kept for testing. The set named used makes sure that no message is in both files. random.seed(0) makes the program choose the same way on every run, so your files are the same as ours.
🇹🇭 ไทย
ตัวอย่างหนึ่งชุดมีสองส่วน คือข้อความ กับออร์เดอร์ที่ข้อความนั้นหมายถึง คุณจะเขียนสี่ร้อยชุดด้วยมือก็ได้ แต่ให้โปรแกรมเขียนเร็วกว่า และโปรแกรมไม่มีทางจับคู่ข้อความกับออร์เดอร์ผิด เพราะมันเริ่มจากออร์เดอร์ก่อน แล้วค่อยเขียนข้อความให้ออร์เดอร์นั้น
random_order สุ่มเลือกคำตอบทั้งห้าค่า จากนั้น message_for เขียนวิธีหนึ่งที่ลูกค้าอาจพูดออร์เดอร์นั้น มันเลือกคำสำหรับจำนวน เลือกหนึ่งในสามวิธีเรียกอาหาร เลือกวลีสำหรับความเผ็ดและวลีสำหรับสถานที่ เลือกคำขึ้นต้นและคำลงท้าย แล้วเรียงความเผ็ดกับสถานที่ตามลำดับที่สุ่มได้ เป็นครั้งคราวมันจะเขียนทั้งข้อความโดยไม่มีตัวพิมพ์ใหญ่เลย อย่างที่คนจริง ๆ พิมพ์กัน
มีกฎสามข้อซ่อนอยู่ในลิสต์คำ และกฎเหล่านี้สำคัญ วลีหนึ่งของ medium คือข้อความว่าง "" ซึ่งหมายความว่าลูกค้าไม่ได้พูดถึงความเผ็ดเลย วลีหนึ่งของการกินที่ร้านก็ว่างเหมือนกัน และ a เป็นคำหนึ่งที่ใช้แทนเลข 1 ตัวอย่างจึงพาธรรมเนียมของร้านติดไปด้วย คือ ไม่พูดถึงความเผ็ดแปลว่าเผ็ดกลาง และไม่พูดถึงสถานที่แปลว่ากินที่ร้าน จะไม่มีใครบอกกฎเหล่านี้กับโมเดลตรง ๆ เลย มันต้องหาเอาเองจากตัวอย่าง
โปรแกรมเขียนข้อความที่ไม่ซ้ำกัน 450 ข้อ 400 ข้อแรกลงไปอยู่ใน train.jsonl และโมเดลจะเรียนจากชุดนี้ อีก 50 ข้อลงไปอยู่ใน test_generated.jsonl และเก็บไว้สำหรับทดสอบ เซตชื่อ used ทำให้แน่ใจว่าไม่มีข้อความไหนอยู่ในทั้งสองไฟล์ ส่วน random.seed(0) ทำให้โปรแกรมเลือกแบบเดิมทุกครั้งที่รัน ไฟล์ของคุณจึงเหมือนของเรา
🇨🇳 中文
一个样例有两部分:一条消息,以及这条消息所表示的订单。四百个样例,你可以用手写。用程序写更快,而且它绝不会把消息和订单配错,因为它是先有订单,再为订单写消息。
random_order 随机选出五个答案。然后 message_for 写出顾客可能的一种说法。它为数量挑一个词,从三种叫法里挑一种来说食物,为辣度挑一个短语,为用餐地点挑一个短语,再挑一个开头和一个结尾,并把辣度和地点按随机的顺序排好。偶尔,它会把整条消息都写成小写,就像人们常做的那样。
词表里藏着三条规则,它们很要紧。表示 medium 的短语里,有一个是空文本 "",意思是顾客根本没提辣度。表示在店里吃的短语里,也有一个是空的。而 a 是表示数字 1 的词之一。于是,样例本身就带着这家店的习惯:没提辣度就是中辣,没提地点就是在店里吃。没有人会把这些规则直接告诉模型,它得自己从样例里找出来。
程序写出 450 条互不相同的消息。前 400 条进了 train.jsonl,模型就从这些样例里学习。另外 50 条进了 test_generated.jsonl,留着做测试。名叫 used 的集合保证没有哪条消息同时出现在两个文件里。random.seed(0) 让程序每次运行都做出同样的选择,所以你的文件和我们的一样。
💻 RUN IT
venv\Scripts\python make_data.py
📄 NEW FILE · make_data.py
"""make_data.py - write the examples the model will learn from.
python make_data.py
One example is a customer's message and the order it means. A program writes
them, so there can be as many as we like. It writes two files:
train.jsonl 400 examples to learn from
test_generated.jsonl 50 more, made the same way and kept for testing
"""
import json
import random
DISHES = ["khao soi", "nam ngiao", "pad thai", "pad see ew"]
PROTEINS = ["chicken", "pork", "beef", "tofu"]
# The words a customer might use for each answer. "" means they said nothing.
SPICE_WORDS = {
"none": ["not spicy", "no chilli", "no spice"],
"mild": ["mild", "a little spicy", "not too spicy"],
"medium": ["medium", "medium spicy", ""],
"hot": ["hot", "very spicy", "extra spicy"],
}
PLACE_WORDS = {
True: ["to take away", "takeaway", "to go"],
False: ["for here", "eating here", "eat in", ""],
}
COUNT_WORDS = {
1: ["1", "one", "a"], 2: ["2", "two"], 3: ["3", "three"],
4: ["4", "four"], 5: ["5", "five"], 6: ["6", "six"],
}
OPENINGS = ["", "", "Hi, ", "Hello! ", "Can I get ", "I'd like ", "I want "]
ENDINGS = ["", "", " please", ", please.", ". Thank you!", " thanks"]
def random_order():
"""One order, with every answer picked at random."""
return {
"dish": random.choice(DISHES),
"protein": random.choice(PROTEINS),
"spice": random.choice(list(SPICE_WORDS)), # one of its four keys
"count": random.randint(1, 6),
"takeaway": random.choice([True, False]),
}
def message_for(order):
"""One way a customer might write this order."""
how_many = random.choice(COUNT_WORDS[order["count"]])
dish = order["dish"]
protein = order["protein"]
food = random.choice([
f"{how_many} {dish} with {protein}",
f"{how_many} {dish} {protein}",
f"{how_many} {protein} {dish}",
])
extras = [
random.choice(SPICE_WORDS[order["spice"]]),
random.choice(PLACE_WORDS[order["takeaway"]]),
]
random.shuffle(extras) # spice first or place first
parts = [food]
for words in extras:
if words: # "" adds nothing to the message
parts.append(words)
text = random.choice(OPENINGS) + ", ".join(parts) + random.choice(ENDINGS)
if random.random() < 0.3:
return text.lower() # some people never type a capital letter
return text[0].upper() + text[1:]
def write_file(name, examples):
with open(name, "w", encoding="utf-8") as file:
for example in examples:
file.write(json.dumps(example) + "\n")
print(f"wrote {name}: {len(examples)} examples")
def main():
random.seed(0) # the same seed writes the same files every time
examples = []
used = set()
while len(examples) < 450:
order = random_order()
message = message_for(order)
if message in used: # so that no message is in both files
continue
used.add(message)
examples.append({"message": message, "order": order})
write_file("train.jsonl", examples[:400])
write_file("test_generated.jsonl", examples[400:])
print()
for example in examples[:4]:
print(example["message"])
print(" ", json.dumps(example["order"]))
if __name__ == "__main__":
main()
🖨️ WHAT IT PRINTED
wrote train.jsonl: 400 examples
wrote test_generated.jsonl: 50 examples
can i get three pad see ew tofu, eat in, no chilli
{"dish": "pad see ew", "protein": "tofu", "spice": "none", "count": 3, "takeaway": false}
Hi, 3 beef khao soi, takeaway, a little spicy, please.
{"dish": "khao soi", "protein": "beef", "spice": "mild", "count": 3, "takeaway": true}
I'd like 5 tofu nam ngiao, hot, for here thanks
{"dish": "nam ngiao", "protein": "tofu", "spice": "hot", "count": 5, "takeaway": false}
I want 6 pad see ew with beef, eating here, not too spicy
{"dish": "pad see ew", "protein": "beef", "spice": "mild", "count": 6, "takeaway": false}
🇬🇧 English
You now have two tests, and they answer different questions. The 50 generated messages are new to the model, but the same program wrote them, with the same phrases as the training examples. A good score there says that the model learned what the examples showed. The 50 handwritten messages were written by a person, before any training, in the words a person uses. A good score there says that the model can do the job for a real customer. Expect the second score to be lower, and expect it to teach you more.
🇹🇭 ไทย
ตอนนี้คุณมีชุดทดสอบสองชุด และมันตอบคำถามคนละข้อ ข้อความ 50 ข้อที่โปรแกรมสร้างนั้นใหม่สำหรับโมเดล แต่โปรแกรมตัวเดียวกันเป็นคนเขียน ด้วยวลีชุดเดียวกับตัวอย่างที่ใช้ฝึก คะแนนดีในชุดนี้บอกว่าโมเดลเรียนรู้สิ่งที่ตัวอย่างแสดงให้เห็น ส่วนข้อความ 50 ข้อที่คนเขียนเองนั้น เขียนไว้ก่อนการฝึกใด ๆ ด้วยถ้อยคำที่คนใช้จริง คะแนนดีในชุดนี้บอกว่าโมเดลทำงานนี้ให้ลูกค้าจริงได้ ให้คาดไว้ว่าคะแนนชุดที่สองจะต่ำกว่า และคาดไว้ด้วยว่ามันจะสอนคุณได้มากกว่า
🇨🇳 中文
现在你有两份测试,它们回答的是不同的问题。那 50 条由程序生成的消息,模型没见过,但写它们的是同一个程序,用的短语也和训练样例一样。在这份测试上得分高,说明模型学会了样例所展示的东西。另外 50 条手写的消息,是人在任何训练之前,用人平常说话的方式写的。在这份测试上得分高,说明模型能为真正的顾客做好这件事。第二个分数多半会低一些,但它能教给你的也更多。
train.jsonl with 400 examples to learn from, and test_generated.jsonl with 50 to test on. If you would rather not run the program, here are ours: train.jsonl and test_generated.jsonl.ไฟล์ใหม่สองไฟล์ข้างชุดทดสอบที่คุณดาวน์โหลดไว้: train.jsonl มีตัวอย่าง 400 ชุดไว้ให้เรียน และ test_generated.jsonl มี 50 ข้อไว้ทดสอบ ถ้าคุณไม่อยากรันโปรแกรมเอง นี่คือไฟล์ของเรา: train.jsonl และ test_generated.jsonl在你下载的那份测试旁边,多了两个新文件:train.jsonl,里面有 400 个用来学习的样例;test_generated.jsonl,里面有 50 条用来测试的消息。如果你不想自己运行这个程序,这是我们的文件:train.jsonl 和 test_generated.jsonl。
5 See an example the way the model sees it · ดูตัวอย่างแบบที่โมเดลเห็น · 像模型那样看一个样例
🇬🇧 English
The model does not read letters or words. It reads tokens, and each token is just a number. tokens.py takes the first training example and shows all of it: first the text, then the tokens.
The text has two parts, and the difference between them is the most important idea in the training file that follows. The first part, question_text, is what the model will read: the instruction, the customer's message, and the marker that says the assistant speaks next. The second part, answer_text, is what the model should write: the order as JSON, and then one special token, <|im_end|>.
That last token means this turn is finished
. It is in the answer on purpose. A model that never practised writing it would never learn to stop, and would run on after the order as it did in step 2.
🇹🇭 ไทย
โมเดลไม่ได้อ่านตัวอักษรหรือคำ มันอ่านโทเคน และโทเคนแต่ละตัวก็เป็นแค่ตัวเลขหนึ่งตัว tokens.py หยิบตัวอย่างสำหรับฝึกชุดแรกมาแสดงให้ดูทั้งหมด เริ่มจากข้อความ แล้วตามด้วยโทเคน
ข้อความมีสองส่วน และความต่างระหว่างสองส่วนนี้คือแนวคิดที่สำคัญที่สุดในไฟล์ฝึกที่กำลังจะตามมา ส่วนแรกคือ question_text เป็นสิ่งที่โมเดลจะอ่าน ได้แก่คำสั่ง ข้อความของลูกค้า และเครื่องหมายที่บอกว่าต่อไปเป็นตาของ assistant ส่วนที่สองคือ answer_text เป็นสิ่งที่โมเดลควรเขียน ได้แก่ออร์เดอร์ในรูป JSON แล้วตามด้วยโทเคนพิเศษหนึ่งตัว คือ <|im_end|>
โทเคนตัวสุดท้ายนั้นหมายความว่า ตานี้จบแล้ว
มันอยู่ในคำตอบโดยตั้งใจ โมเดลที่ไม่เคยฝึกเขียนโทเคนนี้จะไม่มีวันเรียนรู้ที่จะหยุด และจะเขียนเลยออร์เดอร์ไปเรื่อย ๆ เหมือนในขั้นที่ 2
🇨🇳 中文
模型读的不是字母,也不是单词。它读的是 token,而每个 token 只是一个数字。tokens.py 取出第一个训练样例,把它完整地显示出来:先是文字,然后是 token。
这段文字有两部分,而这两部分的区别,就是后面那个训练文件里最重要的想法。第一部分 question_text 是模型要读的内容:指令、顾客的消息,以及表示接下来轮到 assistant 说话的标记。第二部分 answer_text 是模型应该写出来的内容:JSON 形式的订单,后面再跟一个特殊的 token:<|im_end|>。
最后这个 token 的意思是这一轮结束了
。它是特意放进答案里的。一个从没练习过写它的模型,永远学不会停下来,会像第 2 步里那样,写完订单还一直写下去。
💻 RUN IT
venv\Scripts\python tokens.py
📄 NEW FILE · tokens.py
"""tokens.py - show one training example the way the model sees it.
python tokens.py
It needs train.jsonl, which make_data.py writes.
"""
import json
from transformers import AutoTokenizer
MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
INSTRUCTION = "You take orders for a noodle shop. Reply with the order as JSON."
def main():
tokenizer = AutoTokenizer.from_pretrained(MODEL)
with open("train.jsonl", encoding="utf-8") as file:
example = json.loads(file.readline()) # the first example only
chat = [
{"role": "system", "content": INSTRUCTION},
{"role": "user", "content": example["message"]},
]
question_text = tokenizer.apply_chat_template(
chat, tokenize=False, add_generation_prompt=True)
# The end-of-text token is part of the answer: it teaches the model to stop.
answer_text = json.dumps(example["order"]) + tokenizer.eos_token
print(question_text + answer_text)
print()
# The template has already written the special tokens, so add no more.
question_tokens = tokenizer(question_text, add_special_tokens=False)["input_ids"]
answer_tokens = tokenizer(answer_text, add_special_tokens=False)["input_ids"]
print(f"{len(question_tokens)} tokens for the model to read. First five:")
for token in question_tokens[:5]:
piece = tokenizer.decode([token])
# repr() puts quotes around the piece, so a space at its start can be seen.
print(f"{token:8} {repr(piece)}")
print(f"{len(answer_tokens)} tokens it must learn to write. All of them:")
for token in answer_tokens:
piece = tokenizer.decode([token])
print(f"{token:8} {repr(piece)}")
if __name__ == "__main__":
main()
🖨️ WHAT IT PRINTED
<|im_start|>system
You take orders for a noodle shop. Reply with the order as JSON.<|im_end|>
<|im_start|>user
can i get three pad see ew tofu, eat in, no chilli<|im_end|>
<|im_start|>assistant
{"dish": "pad see ew", "protein": "tofu", "spice": "none", "count": 3, "takeaway": false}<|im_end|>
47 tokens for the model to read. First five:
1 '<|im_start|>'
9690 'system'
198 '\n'
2683 'You'
1188 ' take'
36 tokens it must learn to write. All of them:
39428 '{"'
21490 'dish'
1799 '":'
476 ' "'
16384 'pad'
963 ' see'
297 ' e'
103 'w'
1002 '",'
476 ' "'
24089 'protein'
1799 '":'
476 ' "'
1141 'to'
12809 'fu'
1002 '",'
476 ' "'
2401 'sp'
569 'ice'
1799 '":'
476 ' "'
19444 'none'
1002 '",'
476 ' "'
4639 'count'
1799 '":'
216 ' '
35 '3'
28 ','
476 ' "'
21880 'take'
11408 'away'
1799 '":'
7065 ' false'
109 '}'
2 '<|im_end|>'
🇬🇧 English
The answer is 36 tokens. Some are whole words, like count. Some are pieces: tofu is to and fu, and pad see ew is four tokens. Many begin with a space, because the space in front of a word belongs to that word's token. Training will show the model the 47 tokens of the question and ask it to produce these 36, one after another.
🇹🇭 ไทย
คำตอบมี 36 โทเคน บางตัวเป็นคำทั้งคำ เช่น count บางตัวเป็นชิ้นส่วน: tofu คือ to กับ fu และ pad see ew คือสี่โทเคน หลายตัวขึ้นต้นด้วยช่องว่าง เพราะช่องว่างที่อยู่หน้าคำนับเป็นส่วนหนึ่งของโทเคนของคำนั้น การฝึกจะให้โมเดลดูโทเคน 47 ตัวของคำถาม แล้วขอให้มันเขียนโทเคน 36 ตัวนี้ออกมาทีละตัว
🇨🇳 中文
答案有 36 个 token。有些是完整的词,比如 count。有些是碎片:tofu 是 to 和 fu,pad see ew 是四个 token。很多 token 以空格开头,因为一个词前面的空格属于这个词的 token。训练时,会把问题的 47 个 token 给模型看,再要它一个接一个地写出这 36 个。
🏋️ Tutorial 3 — Training, two ways · บทที่ 3 — การฝึก สองแบบ · 教程 3 —— 两种训练方式
🇬🇧 English
Here is the whole idea of training, before any code. Show the model the question. Ask how likely it thinks each correct token of the answer is. Turn its mistakes into one number, the loss. Work out, for every number in the model, which way to nudge it so that the loss would have been a little smaller. Nudge. Then do the same with the next example. Four hundred examples, four hundred nudges.
🇹🇭 ไทย
นี่คือแนวคิดทั้งหมดของการฝึก ก่อนจะมีโค้ดสักบรรทัด ให้โมเดลดูคำถาม ถามว่ามันคิดว่าโทเคนที่ถูกต้องแต่ละตัวของคำตอบมีโอกาสมากแค่ไหน แปลงความผิดพลาดของมันเป็นตัวเลขตัวเดียว คือค่า loss คำนวณว่าตัวเลขแต่ละตัวในโมเดลควรถูกดันไปทางไหน ค่า loss จึงจะเล็กลงอีกนิด แล้วก็ดัน จากนั้นทำแบบเดียวกันกับตัวอย่างชุดถัดไป สี่ร้อยตัวอย่าง ก็ดันสี่ร้อยครั้ง
🇨🇳 中文
在看任何代码之前,先说清训练的全部想法。把问题给模型看。问它:答案里每一个正确的 token,它认为有多大可能。把它的错误变成一个数字,也就是损失。算出模型里的每个数字该往哪边推一点,损失才会小一点。推。然后拿下一个样例再做一遍。四百个样例,就推四百次。
6 train.py: one loop and one choice · train.py: ลูปเดียว กับทางเลือกเดียว · train.py:一个循环,一个选择
🇬🇧 English
train.py is the longest file on the page, but you have already seen most of it. question_as_tokens and answer_as_tokens make the two lists of tokens from step 5. reply_to asks the model for a reply. The new parts are prepare, which makes the one choice this page is about, and the loop in main.
Read the four lines in the middle of the loop slowly, because they are what training is.
loss = model(input_ids=example_tokens, labels=graded_tokens).lossruns the model on the question and the answer together, and compares what it predicted for each token of the answer with the token that is really there. The result is the loss: close to zero when the model was sure of the right tokens, large when it was not.loss.backward()works out, for every number that is allowed to change, whether making it a little bigger or a little smaller would have made the loss smaller.optimizer.step()moves each of those numbers a small way in that direction. How small is set by the learning rate.optimizer.zero_grad()clears those directions, so that the next example starts clean.
graded_tokens is how the model is told which tokens count. It is a copy of the example's tokens in which every token of the question has been replaced by -100. PyTorch's loss skips a position marked -100, so the model is graded on the answer only. We do not want it to learn to write customers' messages. We want it to learn to answer them.
prepare is short, and it is the entire difference between the two kinds of fine-tuning. Given full, it hands the model back untouched, and all of its numbers can change. Given lora, it freezes the model and adds small new blocks of numbers beside some of its layers, and only those can change. Everything else in the file is the same for both: the examples, the loss, the loop. One other thing depends on the choice, the learning rate in LEARNING_RATE. When every number moves, each one must move less, so the full way uses a rate ten times smaller.
Every 25 steps the file prints the average loss of those steps, and the model's reply at that moment to one message, WATCHED, which is not in the training file. You can watch the reply turn into an order.
🇹🇭 ไทย
train.py เป็นไฟล์ที่ยาวที่สุดในหน้านี้ แต่คุณเห็นเกือบทั้งหมดมาแล้ว question_as_tokens กับ answer_as_tokens สร้างลิสต์โทเคนสองชุดจากขั้นที่ 5 reply_to ขอคำตอบจากโมเดล ส่วนที่ใหม่คือ prepare ซึ่งเป็นที่ตัดสินใจเรื่องเดียวที่หน้านี้พูดถึง และลูปใน main
อ่านสี่บรรทัดกลางลูปช้า ๆ เพราะสี่บรรทัดนี้คือตัวการฝึกเลย
loss = model(input_ids=example_tokens, labels=graded_tokens).lossรันโมเดลกับคำถามและคำตอบไปพร้อมกัน แล้วเทียบสิ่งที่มันทายไว้สำหรับโทเคนแต่ละตัวของคำตอบกับโทเคนที่อยู่ตรงนั้นจริง ๆ ผลที่ได้คือค่า loss ซึ่งใกล้ศูนย์เมื่อโมเดลมั่นใจในโทเคนที่ถูก และมีค่ามากเมื่อมันไม่มั่นใจloss.backward()คำนวณให้ตัวเลขทุกตัวที่ได้รับอนุญาตให้เปลี่ยนว่า ถ้าทำให้มันใหญ่ขึ้นนิดหรือเล็กลงนิด แบบไหนจะทำให้ค่า loss เล็กลงoptimizer.step()ขยับตัวเลขแต่ละตัวไปทางนั้นเป็นระยะสั้น ๆ สั้นแค่ไหนกำหนดด้วย learning rateoptimizer.zero_grad()ล้างทิศทางเหล่านั้นทิ้ง เพื่อให้ตัวอย่างถัดไปเริ่มต้นใหม่อย่างสะอาด
graded_tokens คือวิธีบอกโมเดลว่าโทเคนตัวไหนนับคะแนน มันเป็นสำเนาของโทเคนของตัวอย่าง โดยโทเคนทุกตัวของคำถามถูกแทนด้วย -100 ค่า loss ของ PyTorch จะข้ามตำแหน่งที่มีเครื่องหมาย -100 โมเดลจึงถูกให้คะแนนจากคำตอบเท่านั้น เราไม่ได้อยากให้มันเรียนวิธีเขียนข้อความของลูกค้า เราอยากให้มันเรียนวิธีตอบข้อความเหล่านั้น
prepare สั้น และมันคือความต่างทั้งหมดระหว่างการปรับจูนสองแบบ เมื่อได้รับ full มันส่งโมเดลกลับมาโดยไม่แตะต้องอะไร และตัวเลขทั้งหมดของโมเดลเปลี่ยนได้ เมื่อได้รับ lora มันล็อกโมเดลไว้ แล้วเพิ่มกลุ่มตัวเลขใหม่ขนาดเล็กไว้ข้างเลเยอร์บางชั้น และมีแต่ตัวเลขกลุ่มนี้ที่เปลี่ยนได้ อย่างอื่นทั้งหมดในไฟล์เหมือนกันสำหรับทั้งสองแบบ ทั้งตัวอย่าง ค่า loss และลูป มีอีกอย่างเดียวที่ขึ้นกับตัวเลือกนี้ คือ learning rate ใน LEARNING_RATE เมื่อตัวเลขทุกตัวขยับ แต่ละตัวต้องขยับน้อยลง แบบเต็มจึงใช้ค่าที่เล็กกว่าสิบเท่า
ทุก 25 สเต็ป ไฟล์จะพิมพ์ค่า loss เฉลี่ยของสเต็ปเหล่านั้น และคำตอบของโมเดล ณ ตอนนั้นต่อข้อความหนึ่งข้อ คือ WATCHED ซึ่งไม่ได้อยู่ในไฟล์ฝึก คุณจะได้เฝ้าดูคำตอบค่อย ๆ กลายเป็นออร์เดอร์
🇨🇳 中文
train.py 是这一页里最长的文件,但其中大部分你已经见过了。question_as_tokens 和 answer_as_tokens 生成第 5 步里的那两个 token 列表。reply_to 让模型给出回复。新的部分是 prepare,这一页要讲的那个选择就在它里面,还有 main 里的循环。
循环中间的四行,请慢慢读,因为训练就是这四行。
loss = model(input_ids=example_tokens, labels=graded_tokens).loss把问题和答案一起交给模型运行,再把它对答案里每个 token 的预测,和那个位置上真正的 token 比较。结果就是损失:模型对正确的 token 很有把握时,它接近零;没把握时,它就大。loss.backward()为每一个允许改动的数字算出:把它调大一点还是调小一点,损失才会变小。optimizer.step()把这些数字各自朝那个方向挪动一小步。这一步有多小,由学习率决定。optimizer.zero_grad()把这些方向清掉,让下一个样例从干净的状态开始。
graded_tokens 用来告诉模型哪些 token 算分。它是样例 token 的一份副本,其中属于问题的每个 token 都换成了 -100。PyTorch 的损失会跳过标着 -100 的位置,所以模型只在答案上被打分。我们不想让它学会写顾客的消息,我们想让它学会回答这些消息。
prepare 很短,它就是两种微调之间的全部差别。传给它 full,它把模型原样交回,模型的所有数字都可以变。传给它 lora,它把模型冻结,在某些层旁边加上一小块一小块新的数字,只有这些新数字可以变。文件里其余的一切,两种方式都一样:样例、损失、循环。只有另一样东西取决于这个选择,那就是 LEARNING_RATE 里的学习率。每个数字都在动的时候,每一个就得少动一点,所以全量的方式用的学习率小十倍。
每过 25 个训练步,文件会打印这些步的平均损失,以及模型此刻对一条消息的回复。这条消息就是 WATCHED,它不在训练文件里。你可以看着回复一点点变成一份订单。
📄 NEW FILE · train.py
"""train.py - fine-tune the model on train.jsonl, in one of two ways.
python train.py lora train a small add-on; the model itself stays as it is
python train.py full train every number in the model
The result is saved in a folder: tuned_lora or tuned_full.
"""
import json
import sys
import time
import torch
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
INSTRUCTION = "You take orders for a noodle shop. Reply with the order as JSON."
# How far one step may move a number. When every number moves, each must move less.
LEARNING_RATE = {"lora": 0.0002, "full": 0.00002}
WATCHED = "Two khao soi with chicken, not too spicy, to take away please."
def read_examples(name):
"""Every line of the file is one example, written as JSON."""
examples = []
with open(name, encoding="utf-8") as file:
for line in file:
examples.append(json.loads(line))
return examples
def question_as_tokens(tokenizer, message):
"""The tokens the model reads: the instruction, the message, and "your turn"."""
chat = [
{"role": "system", "content": INSTRUCTION},
{"role": "user", "content": message},
]
question_text = tokenizer.apply_chat_template(
chat, tokenize=False, add_generation_prompt=True)
return tokenizer(question_text, add_special_tokens=False)["input_ids"]
def answer_as_tokens(tokenizer, order):
"""The tokens the model should write: the order, then the token that says stop."""
answer_text = json.dumps(order) + tokenizer.eos_token
return tokenizer(answer_text, add_special_tokens=False)["input_ids"]
def reply_to(model, tokenizer, message):
"""The model's reply to one message, on one line."""
question_tokens = question_as_tokens(tokenizer, message)
# The model takes rows of tokens. The extra brackets make this list one row.
output = model.generate(
torch.tensor([question_tokens]), max_new_tokens=50, do_sample=False)
reply_tokens = output[0][len(question_tokens):]
reply = tokenizer.decode(reply_tokens, skip_special_tokens=True)
return " ".join(reply.split())
def count_numbers(tensors):
"""How many numbers these blocks of numbers hold between them."""
total = 0
for tensor in tensors:
total = total + tensor.numel()
return total
def prepare(model, kind):
"""Decide which numbers training may change. This is the whole difference."""
if kind == "full":
return model # as it arrived: every number may change
config = LoraConfig(
r=8, # the size of each add-on
lora_alpha=16, # how loudly the add-ons speak
target_modules=["q_proj", "v_proj"], # the layers that get one
task_type="CAUSAL_LM", # a model that writes text
)
return get_peft_model(model, config) # freezes the model, then adds them
def main():
kind = sys.argv[1] # the word after train.py: "lora" or "full"
# The add-ons start as random numbers. This makes them the same on every run.
torch.manual_seed(0)
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32)
model = prepare(model, kind)
trainable = []
for tensor in model.parameters(): # a tensor is one block of numbers
if tensor.requires_grad: # False for a frozen block
trainable.append(tensor)
print(f"{kind}: training can change {count_numbers(trainable):,}"
f" of {count_numbers(model.parameters()):,} numbers")
optimizer = torch.optim.AdamW(trainable, lr=LEARNING_RATE[kind])
examples = read_examples("train.jsonl")
print(f"step 0 {reply_to(model, tokenizer, WATCHED)}")
started = time.perf_counter()
recent_losses = []
step = 0
for example in examples:
step = step + 1
question_tokens = question_as_tokens(tokenizer, example["message"])
answer_tokens = answer_as_tokens(tokenizer, example["order"])
example_tokens = torch.tensor([question_tokens + answer_tokens])
# -100 means "do not grade this token". Only the answer is graded.
graded_tokens = torch.tensor(
[[-100] * len(question_tokens) + answer_tokens])
# How wrong was the model on this example?
loss = model(input_ids=example_tokens, labels=graded_tokens).loss
# For each number that may change: which way would have been less wrong?
loss.backward()
# Move each of those numbers a little that way.
optimizer.step()
# Clear the directions, ready for the next example.
optimizer.zero_grad()
recent_losses.append(loss.item()) # .item() is the loss as a plain number
if step % 25 == 0: # every 25 steps, say how it is going
average = sum(recent_losses) / len(recent_losses)
recent_losses = []
print(f"step {step:3} loss {average:.3f}"
f" {reply_to(model, tokenizer, WATCHED)}")
seconds = time.perf_counter() - started
print(f"{step} steps in {seconds:.0f} seconds")
model.save_pretrained(f"tuned_{kind}")
print(f"saved in the folder tuned_{kind}")
if __name__ == "__main__":
main()
7 The full way: every number can change · แบบเต็ม: ตัวเลขทุกตัวเปลี่ยนได้ · 全量的方式:每个数字都可以变
🇬🇧 English
Start with the full way, because it has no new idea in it. It is simply more of the training that made the model in the first place.
🇹🇭 ไทย
เริ่มจากแบบเต็มก่อน เพราะมันไม่มีแนวคิดใหม่อะไรเลย มันก็แค่การฝึกแบบเดียวกับที่สร้างโมเดลนี้ขึ้นมาตั้งแต่แรก ทำต่ออีกหน่อย
🇨🇳 中文
先从全量的方式开始,因为它没有任何新的想法。它只不过是把当初造出这个模型的那种训练,再多做一些。
💻 RUN IT
venv\Scripts\python train.py full
🖨️ WHAT IT PRINTED
full: training can change 134,515,008 of 134,515,008 numbers
step 0 { "name": "Khao Soi with Chicken", "quantity": 2, "price": 10.99, "quantity_per_person": 2, "price_per_person":
step 25 loss 0.486 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 50 loss 0.067 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 75 loss 0.027 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 100 loss 0.018 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 125 loss 0.029 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 150 loss 0.010 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 175 loss 0.003 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 200 loss 0.001 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 225 loss 0.001 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 250 loss 0.001 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 275 loss 0.000 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 300 loss 0.000 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 325 loss 0.000 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 350 loss 0.000 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 375 loss 0.000 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 400 loss 0.000 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
400 steps in 431 seconds
saved in the folder tuned_full
🇬🇧 English
The first line says it all: training can change 134,515,008 of 134,515,008 numbers. At step 0 the reply is the same invented JSON as in step 2. Twenty-five examples later the model already writes the shop's five names, though it reads not too spicy
as no spice at all. From step 75 on, the reply is right every time, and the loss keeps falling towards zero.
The run took 431 seconds on our laptop and held 2.7 GB of memory. It saved a folder named tuned_full that holds a complete model of 538.1 MB. That is about twice the size of the download, because the download stores 16-bit numbers and we trained and saved 32-bit ones.
🇹🇭 ไทย
บรรทัดแรกบอกทุกอย่าง: การฝึกเปลี่ยนตัวเลขได้ 134,515,008 จาก 134,515,008 ตัว ที่สเต็ป 0 คำตอบยังเป็น JSON ที่แต่งขึ้นเองแบบเดียวกับในขั้นที่ 2 ผ่านไปยี่สิบห้าตัวอย่าง โมเดลก็เขียนชื่อทั้งห้าชื่อของร้านได้แล้ว แม้จะยังอ่าน not too spicy
ว่าไม่เผ็ดเลย ตั้งแต่สเต็ป 75 เป็นต้นไป คำตอบถูกทุกครั้ง และค่า loss ก็ลดลงเข้าหาศูนย์เรื่อย ๆ
การรันครั้งนี้ใช้เวลา 431 วินาทีบนแล็ปท็อปของเรา และใช้หน่วยความจำ 2.7 GB มันบันทึกโฟลเดอร์ชื่อ tuned_full ที่มีโมเดลครบทั้งตัว ขนาด 538.1 MB ซึ่งใหญ่กว่าไฟล์ที่ดาวน์โหลดมาราวสองเท่า เพราะไฟล์ที่ดาวน์โหลดเก็บตัวเลข 16 บิต แต่เราฝึกและบันทึกเป็นตัวเลข 32 บิต
🇨🇳 中文
第一行就说明了一切:训练可以改动 134,515,008 个数字中的 134,515,008 个。在第 0 步,回复还是第 2 步里那种自己编出来的 JSON。只看了二十五个样例,模型就已经写出店里要的五个名称,不过它把 not too spicy
理解成了完全不辣。从第 75 个训练步起,回复每次都对,损失也一直朝零降下去。
这次运行在我们的笔记本上用了 431 秒,占了 2.7 GB 内存。它保存了一个名叫 tuned_full 的文件夹,里面是一个完整的模型,538.1 MB。这大约是下载文件的两倍,因为下载的文件存的是 16 位的数字,而我们训练和保存用的是 32 位的。
8 The LoRA way: freeze the model, train a small add-on · แบบ LoRA: ล็อกโมเดลไว้ แล้วฝึกตัวเสริมขนาดเล็ก · LoRA 的方式:冻结模型,训练一个小小的附加模块
🇬🇧 English
Now the other choice. LoRA starts from one observation: to teach a trained model a new job, you do not need the freedom to move every number separately. A much smaller change is enough.
Most of a model's numbers sit in large tables called matrices. One of the matrices in this model, named q_proj, has 576 rows and 576 columns, which makes 331,776 numbers. Full fine-tuning may change each one. LoRA leaves the table exactly as it is and puts two thin tables beside it: one with 576 rows and r columns, the other with r rows and 576 columns. Multiplied together, those two give a full table of 576 by 576 changes, and that is added to what the frozen table does. With r = 8 the two thin tables hold 9,216 numbers between them, and only those are trained.
r is called the rank, and it is the size of the add-on. A bigger r gives the add-on more freedom and more numbers. lora_alpha sets how loudly the add-on speaks: its change is multiplied by lora_alpha / r, which here is 16 / 8 = 2. target_modules says which matrices get an add-on. q_proj and v_proj are two of the matrices in the part of each block called attention, where the model decides which earlier tokens matter for the next one. This model has 30 blocks, so it gets 60 add-ons.
🇹🇭 ไทย
ทีนี้มาดูอีกทางเลือกหนึ่ง LoRA เริ่มจากข้อสังเกตข้อเดียว คือ การจะสอนงานใหม่ให้โมเดลที่ฝึกมาแล้ว คุณไม่จำเป็นต้องมีอิสระที่จะขยับตัวเลขทุกตัวแยกกัน การเปลี่ยนแปลงที่เล็กกว่านั้นมากก็เพียงพอ
ตัวเลขส่วนใหญ่ของโมเดลอยู่ในตารางขนาดใหญ่ที่เรียกว่าเมทริกซ์ เมทริกซ์ตัวหนึ่งในโมเดลนี้ชื่อ q_proj มี 576 แถวและ 576 คอลัมน์ รวมเป็นตัวเลข 331,776 ตัว การปรับจูนแบบเต็มเปลี่ยนได้ทุกตัว ส่วน LoRA ปล่อยตารางนี้ไว้อย่างเดิมทุกประการ แล้ววางตารางบาง ๆ สองตารางไว้ข้างกัน ตารางหนึ่งมี 576 แถวกับ r คอลัมน์ อีกตารางมี r แถวกับ 576 คอลัมน์ เมื่อคูณสองตารางนี้เข้าด้วยกัน จะได้ตารางการเปลี่ยนแปลงขนาดเต็ม 576 คูณ 576 ซึ่งถูกบวกเข้ากับสิ่งที่ตารางที่ล็อกไว้ทำอยู่ เมื่อ r = 8 ตารางบางสองตารางมีตัวเลขรวมกัน 9,216 ตัว และมีแต่ตัวเลขเหล่านี้ที่ถูกฝึก
r เรียกว่า rank และมันคือขนาดของตัวเสริม r ที่ใหญ่ขึ้นให้ตัวเสริมมีอิสระมากขึ้นและมีตัวเลขมากขึ้น lora_alpha กำหนดว่าตัวเสริมพูดเสียงดังแค่ไหน การเปลี่ยนแปลงของมันจะถูกคูณด้วย lora_alpha / r ซึ่งในที่นี้คือ 16 / 8 = 2 ส่วน target_modules บอกว่าเมทริกซ์ตัวไหนจะได้ตัวเสริม q_proj กับ v_proj เป็นเมทริกซ์สองตัวในส่วนของแต่ละบล็อกที่เรียกว่า attention ซึ่งเป็นที่ที่โมเดลตัดสินว่าโทเคนก่อนหน้าตัวไหนสำคัญต่อโทเคนถัดไป โมเดลนี้มี 30 บล็อก จึงได้ตัวเสริม 60 ตัว
🇨🇳 中文
现在看另一个选择。LoRA 从一个观察出发:要教一个已经训练好的模型做一件新事,并不需要能够单独挪动每一个数字的那种自由。小得多的改动就够了。
模型的大部分数字都放在叫作矩阵的大表格里。这个模型里有一个矩阵叫 q_proj,有 576 行、576 列,一共 331,776 个数字。全量微调可以改动其中每一个。LoRA 让这张表格保持原样,在它旁边放两张很薄的表格:一张有 576 行、r 列,另一张有 r 行、576 列。把这两张相乘,就得到一张 576 乘 576 的、完整大小的“改动表”,它被加到那张冻结的表格所做的事情上。r = 8 时,两张薄表格加起来有 9,216 个数字,被训练的只有这些。
r 叫作秩(rank),它就是附加模块的大小。r 越大,附加模块的自由度越大,数字也越多。lora_alpha 决定附加模块“说话的音量”:它带来的改动要乘上 lora_alpha / r,在这里是 16 / 8 = 2。target_modules 指明哪些矩阵要加附加模块。q_proj 和 v_proj 是每一层里叫作注意力(attention)的那一部分中的两个矩阵,模型在那里决定前面的哪些 token 对下一个 token 重要。这个模型有 30 层,所以一共得到 60 个附加模块。
One matrix and its add-on, drawn to scaleเมทริกซ์หนึ่งตัวกับตัวเสริมของมัน วาดตามสัดส่วนจริง一个矩阵和它的附加模块,按真实比例画出
The grey square is q_proj, frozen. The two blue strips are its add-on, and they are all that training changes. Move the slider to change r.สี่เหลี่ยมสีเทาคือ q_proj ที่ถูกล็อกไว้ แถบสีน้ำเงินสองแถบคือตัวเสริมของมัน และเป็นทั้งหมดที่การฝึกเปลี่ยน เลื่อนแถบเลื่อนเพื่อเปลี่ยนค่า r灰色的方块是被冻结的 q_proj。两条蓝色的细条是它的附加模块,训练改动的只有它们。拖动滑块可以改变 r。
🇬🇧 English
The counts under the picture are not worked out by the page. A script built the add-ons for real at each of the seven sizes and counted their numbers, and the page reads its output, which is folded away just below. The second matrix, v_proj, is smaller than the one in the picture, 192 rows by 576 columns, which is why 60 add-ons come to less than 60 times 9,216.
One of the two thin tables starts as all zeros, so at step 0 the add-on changes nothing: the first reply below is the same as before. The other starts as random numbers. That is why train.py calls torch.manual_seed(0), which makes those random numbers the same on every run.
🇹🇭 ไทย
จำนวนที่อยู่ใต้ภาพไม่ได้คำนวณโดยหน้านี้ สคริปต์ตัวหนึ่งสร้างตัวเสริมขึ้นมาจริง ๆ ที่ขนาดทั้งเจ็ดขนาด แล้วนับตัวเลขของมัน และหน้านี้อ่านผลลัพธ์ของสคริปต์นั้น ซึ่งพับเก็บไว้ข้างล่างนี้ เมทริกซ์ตัวที่สองคือ v_proj เล็กกว่าตัวที่อยู่ในภาพ คือ 192 แถวคูณ 576 คอลัมน์ นี่คือเหตุผลที่ตัวเสริม 60 ตัวรวมกันได้น้อยกว่า 60 เท่าของ 9,216
ตารางบางหนึ่งในสองตารางเริ่มต้นเป็นศูนย์ทั้งหมด ที่สเต็ป 0 ตัวเสริมจึงไม่เปลี่ยนอะไรเลย คำตอบแรกข้างล่างจึงเหมือนกับก่อนหน้านี้ อีกตารางหนึ่งเริ่มต้นเป็นตัวเลขสุ่ม นี่คือเหตุผลที่ train.py เรียก torch.manual_seed(0) ซึ่งทำให้ตัวเลขสุ่มเหล่านั้นเหมือนเดิมทุกครั้งที่รัน
🇨🇳 中文
图下面的那些数量不是这个页面算出来的。有一个脚本在七种大小下都真正建出了附加模块,数了它们的数字;页面读的是这个脚本的输出,就折叠在下面。第二个矩阵 v_proj 比图里的那个小,是 192 行乘 576 列,所以 60 个附加模块加起来不到 9,216 的 60 倍。
两张薄表格里,有一张一开始全是零,所以在第 0 步,附加模块什么都不改变:下面的第一条回复和之前一样。另一张一开始是随机数。这就是 train.py 调用 torch.manual_seed(0) 的原因,它让这些随机数每次运行都一样。
Show the count at every size · แสดงจำนวนที่ทุกขนาด · 显示每种大小下的数量
HuggingFaceTB/SmolLM2-135M-Instruct
134,515,008 numbers, in 30 blocks one after another
In each block, train.py puts an add-on on two matrices:
q_proj: 576 x 576 = 331,776 numbers. Its add-on at r=8: 8 x 576 and 576 x 8 = 9,216 numbers.
v_proj: 192 x 576 = 110,592 numbers. Its add-on at r=8: 8 x 576 and 192 x 8 = 6,144 numbers.
So each 1 in r is 1,920 numbers in a block, and 30 x 1,920 = 57,600 numbers in the model.
r numbers trained by the formula share of the model
1 57,600 57,600 0.04 %
2 115,200 115,200 0.09 %
4 230,400 230,400 0.17 %
8 460,800 460,800 0.34 %
16 921,600 921,600 0.69 %
32 1,843,200 1,843,200 1.37 %
64 3,686,400 3,686,400 2.74 %
With an add-on on all seven kinds of matrix in a block, at r=8: 2,442,240 numbers, 1.82 % of the model.
💻 RUN IT
venv\Scripts\python train.py lora
🖨️ WHAT IT PRINTED
lora: training can change 460,800 of 134,975,808 numbers
step 0 { "name": "Khao Soi with Chicken", "quantity": 2, "price": 10.99, "quantity_per_person": 2, "price_per_person":
step 25 loss 1.634 { "name": "Khao Soi", "type": "Khao Soi", "quantity": 2, "price": 10.99, "delivery_time": "2 hours",
step 50 loss 0.827 {"dish": "khao soi", "protein": "chicken", "spice": "not too spicy", "toast": true, "to go": "tomato"}
step 75 loss 0.230 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 100 loss 0.077 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 125 loss 0.054 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": false}
step 150 loss 0.043 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 175 loss 0.032 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 200 loss 0.020 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 225 loss 0.021 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 250 loss 0.020 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 275 loss 0.016 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 300 loss 0.015 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 325 loss 0.009 {"dish": "khao soi", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
step 350 loss 0.008 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 375 loss 0.005 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
step 400 loss 0.006 {"dish": "khao soi", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": true}
400 steps in 195 seconds
saved in the folder tuned_lora
🇬🇧 English
Training can change 460,800 of 134,975,808 numbers. The second number is larger than before because it counts the add-ons as well as the model. This time you can watch the learning happen in stages. At step 25 the model is still inventing its own JSON. At step 50 it has understood that there is a fixed form and is groping for it: it writes dish and protein and spice, and then "toast": true. At step 75 the form is right. The hard answer, that not too spicy
means mild and not none, goes back and forth, and is right every time only from step 350.
The run took 195 seconds and held 1.1 GB. And the folder it saved, tuned_lora, is 1.9 MB. It does not contain a model. It contains only the add-ons, and it is useless without the original model beside it.
🇹🇭 ไทย
การฝึกเปลี่ยนตัวเลขได้ 460,800 จาก 134,975,808 ตัว ตัวเลขตัวที่สองมากกว่าเมื่อครู่ เพราะมันนับตัวเสริมรวมกับตัวโมเดลด้วย ครั้งนี้คุณจะได้เห็นการเรียนรู้เกิดขึ้นเป็นขั้น ๆ ที่สเต็ป 25 โมเดลยังแต่ง JSON ของตัวเองอยู่ ที่สเต็ป 50 มันเข้าใจแล้วว่ามีรูปแบบตายตัว และกำลังคลำหารูปแบบนั้น มันเขียน dish protein และ spice แล้วตามด้วย "toast": true ที่สเต็ป 75 รูปแบบถูกต้องแล้ว ส่วนคำตอบที่ยาก คือ not too spicy
หมายถึง mild ไม่ใช่ none นั้น ถูกบ้างผิดบ้างสลับไปมา และเพิ่งจะถูกทุกครั้งตั้งแต่สเต็ป 350
การรันครั้งนี้ใช้เวลา 195 วินาที และใช้หน่วยความจำ 1.1 GB ส่วนโฟลเดอร์ที่มันบันทึกไว้ คือ tuned_lora มีขนาด 1.9 MB ในนั้นไม่มีโมเดล มีแต่ตัวเสริม และมันใช้การไม่ได้ถ้าไม่มีโมเดลต้นฉบับอยู่ข้าง ๆ
🇨🇳 中文
训练可以改动 134,975,808 个数字中的 460,800 个。总数比刚才大,是因为它把附加模块和模型本身都算了进去。这一次,你能看到学习是分阶段发生的。训练到第 25 步,模型还在编它自己的 JSON。到第 50 步,它已经明白有一个固定的格式,正在摸索:它写出了 dish、protein 和 spice,接着却写了 "toast": true。到第 75 步,格式对了。最难的那个答案,也就是 not too spicy
表示 mild 而不是 none,时对时错,直到第 350 步起才每次都对。
这次运行用了 195 秒,占了 1.1 GB 内存。而它保存的文件夹 tuned_lora 只有 1.9 MB。里面没有模型,只有附加模块;旁边没有原来的模型,它就毫无用处。
9 LoRA and full fine-tuning, side by side · LoRA กับการปรับจูนแบบเต็ม วางเทียบกัน · LoRA 与全量微调,放在一起比
🇬🇧 English
The two runs used the same model, the same 400 examples in the same order, and the same loop. Only the choice in prepare was different. So they can be laid over each other, step for step. The picture below does that. It reads the two outputs you have just seen, and nothing else.
🇹🇭 ไทย
การรันทั้งสองครั้งใช้โมเดลตัวเดียวกัน ตัวอย่าง 400 ชุดเดียวกันในลำดับเดียวกัน และลูปเดียวกัน ต่างกันแค่ตัวเลือกใน prepare จึงเอามาวางซ้อนกันได้ทีละสเต็ป ภาพข้างล่างทำอย่างนั้น มันอ่านผลลัพธ์สองชุดที่คุณเพิ่งเห็น และไม่ได้อ่านอะไรอื่นเลย
🇨🇳 中文
两次运行用的是同一个模型、同样的 400 个样例、同样的顺序,以及同一个循环。不同的只有 prepare 里的那个选择。所以可以把它们一步对一步地叠在一起看。下面的图做的就是这件事。它读的只是你刚刚看到的那两份输出,别的什么都不读。
The two trainings, step by stepการฝึกสองแบบ ทีละสเต็ป两次训练,一步一步看
The lines show the loss: the average over the 25 steps before each point. Move the slider, or point at the picture, to choose a step and read what each model replied at that moment to Two khao soi with chicken, not too spicy, to take away please.
เส้นแสดงค่า loss คือค่าเฉลี่ยของ 25 สเต็ปก่อนแต่ละจุด เลื่อนแถบเลื่อน หรือชี้ที่ภาพ เพื่อเลือกสเต็ป แล้วอ่านว่า ณ ตอนนั้นโมเดลแต่ละตัวตอบข้อความ Two khao soi with chicken, not too spicy, to take away please.
ว่าอย่างไร两条线是损失:每个点是它之前 25 个训练步的平均值。拖动滑块,或者把指针移到图上,选一个训练步,看看两个模型在那一刻对 Two khao soi with chicken, not too spicy, to take away please.
各回复了什么。
| What we measuredสิ่งที่เราวัด我们测了什么 | Full fine-tuningการปรับจูนแบบเต็ม全量微调 | LoRA |
|---|---|---|
| Numbers that training can changeตัวเลขที่การฝึกเปลี่ยนได้训练可以改动的数字 | 134,515,008, which is all of them134,515,008 ตัว คือทั้งหมด134,515,008 个,也就是全部 | 460,800, which is 0.34 %460,800 ตัว คือ 0.34 %460,800 个,占 0.34 % |
| Time for 400 stepsเวลาที่ใช้ 400 สเต็ป400 个训练步的用时 | 431 seconds431 วินาที431 秒 | 195 seconds195 วินาที195 秒 |
| Most memory heldหน่วยความจำที่ใช้มากที่สุด内存占用的最大值 | 2.7 GB | 1.1 GB |
| What it savesสิ่งที่มันบันทึก它保存了什么 | a whole model, 538.1 MBโมเดลทั้งตัว 538.1 MB一个完整的模型,538.1 MB | the add-ons only, 1.9 MBเฉพาะตัวเสริม 1.9 MB只有附加模块,1.9 MB |
| The watched reply is right at every step fromคำตอบที่เฝ้าดูถูกทุกสเต็ปตั้งแต่被观察的那条回复,从哪一步起每一步都对 | step 75สเต็ป 75第 75 步 | step 350สเต็ป 350第 350 步 |
| Right on the generated test (step 10)ตอบถูกในชุดทดสอบที่โปรแกรมสร้าง (ขั้นที่ 10)生成的测试答对多少(第 10 步) | 50 of 5050 จาก 5050 条里 50 条 | 49 of 5049 จาก 5050 条里 49 条 |
| Right on the handwritten test (step 10)ตอบถูกในชุดทดสอบที่คนเขียนเอง (ขั้นที่ 10)手写的测试答对多少(第 10 步) | 38 of 5038 จาก 5050 条里 38 条 | 39 of 5039 จาก 5050 条里 39 条 |
🇬🇧 English
On this job the two ways ended in the same place. The full way learned in fewer steps, because it had every number to work with. The LoRA way got there too, by moving about one number in three hundred. It took 195 seconds instead of 431 and 1.1 GB of memory instead of 2.7, and what it saved is 288 times smaller.
For a model this small, that difference is a convenience. For a large one, it decides whether you can train at all. Training has to hold in memory, for every number it may change, the number itself, the direction to move it, and the optimizer's notes about it. With full fine-tuning that is the whole model several times over, and for a large model it is more memory than an ordinary graphics card has. Step 12 shows a real case.
There is a second practical difference. A full fine-tune gives you a new model for each job: three jobs, three copies of 538.1 MB. LoRA gives you one model and three small files, and you load the file for the job in hand.
Full fine-tuning still has its place. It learned faster here, and when a job is far from anything the model already does, moving every number can reach further than a small add-on can. But for adapting a model to one job on your own hardware, LoRA is the usual choice, and the table shows why.
🇹🇭 ไทย
ในงานนี้ ทั้งสองแบบไปจบที่จุดเดียวกัน แบบเต็มเรียนรู้ได้ในจำนวนสเต็ปที่น้อยกว่า เพราะมีตัวเลขทุกตัวให้ใช้ แบบ LoRA ก็ไปถึงเหมือนกัน ด้วยการขยับตัวเลขราวหนึ่งตัวในสามร้อยตัว มันใช้เวลา 195 วินาทีแทนที่จะเป็น 431 ใช้หน่วยความจำ 1.1 GB แทนที่จะเป็น 2.7 และสิ่งที่มันบันทึกเล็กกว่ากัน 288 เท่า
สำหรับโมเดลที่เล็กขนาดนี้ ความต่างนั้นเป็นแค่เรื่องความสะดวก แต่สำหรับโมเดลขนาดใหญ่ มันเป็นตัวตัดสินว่าคุณจะฝึกได้หรือไม่ได้เลย การฝึกต้องเก็บไว้ในหน่วยความจำ สำหรับตัวเลขทุกตัวที่มันอาจเปลี่ยน ทั้งตัวเลขนั้นเอง ทิศทางที่จะขยับมัน และบันทึกของ optimizer เกี่ยวกับมัน กับการปรับจูนแบบเต็ม นั่นคือโมเดลทั้งตัวซ้ำกันหลายรอบ และสำหรับโมเดลขนาดใหญ่ มันมากกว่าหน่วยความจำที่การ์ดจอธรรมดามี ขั้นที่ 12 แสดงกรณีจริง
ยังมีความต่างในทางปฏิบัติอีกข้อหนึ่ง การปรับจูนแบบเต็มให้โมเดลใหม่หนึ่งตัวต่อหนึ่งงาน สามงานก็คือสำเนาขนาด 538.1 MB สามชุด LoRA ให้โมเดลตัวเดียวกับไฟล์เล็ก ๆ สามไฟล์ แล้วคุณก็โหลดไฟล์ของงานที่กำลังจะทำ
การปรับจูนแบบเต็มยังมีที่ของมัน ในหน้านี้มันเรียนรู้ได้เร็วกว่า และเมื่องานอยู่ไกลจากทุกอย่างที่โมเดลทำได้อยู่แล้ว การขยับตัวเลขทุกตัวก็ไปได้ไกลกว่าตัวเสริมขนาดเล็ก แต่สำหรับการปรับโมเดลให้เข้ากับงานหนึ่งอย่างบนฮาร์ดแวร์ของคุณเอง LoRA คือตัวเลือกที่ใช้กันทั่วไป และตารางข้างบนก็แสดงเหตุผลไว้แล้ว
🇨🇳 中文
在这个任务上,两种方式到达了同一个地方。全量的方式用更少的步数就学会了,因为它有每一个数字可用。LoRA 的方式也到了,它只挪动了大约三百分之一的数字。它用了 195 秒,而不是 431 秒;占了 1.1 GB 内存,而不是 2.7 GB;它保存的东西小了 288 倍。
对这么小的模型来说,这个差别只是方便与否。对大模型来说,它决定你到底能不能训练。训练时,对每一个可能改动的数字,内存里都要放着这个数字本身、该往哪边挪的方向,以及优化器为它记的笔记。全量微调时,这相当于好几个完整的模型;对大模型来说,这比一块普通显卡的内存还多。第 12 步有一个真实的例子。
还有第二个实际的差别。全量微调给每个任务一个新模型:三个任务,就是三份 538.1 MB 的副本。LoRA 给你一个模型和三个小文件,要做哪个任务,就加载哪个文件。
全量微调仍有它的用处。在这里它学得更快;而当一个任务离模型已经会做的事都很远时,挪动每一个数字能比一个小小的附加模块走得更远。但要在自己的硬件上让模型适应一个任务,LoRA 是通常的选择,上面的表格说明了原因。
🎯 Tutorial 4 — Did it work? · บทที่ 4 — ได้ผลไหม · 教程 4 —— 管用吗?
🇬🇧 English
The loss fell to almost nothing, and the one message we watched came out right. Neither fact proves much. The loss was measured on the training examples, which the model has now seen, and one message is one message. The honest question is what the model does with messages it has never seen.
🇹🇭 ไทย
ค่า loss ลดลงจนแทบไม่เหลือ และข้อความหนึ่งข้อที่เราเฝ้าดูก็ออกมาถูก ข้อเท็จจริงทั้งสองข้อนี้ยังพิสูจน์อะไรได้ไม่มาก ค่า loss วัดจากตัวอย่างที่ใช้ฝึก ซึ่งโมเดลเห็นมาแล้ว และข้อความหนึ่งข้อก็เป็นแค่ข้อความหนึ่งข้อ คำถามที่ตรงไปตรงมาคือ โมเดลทำอย่างไรกับข้อความที่มันไม่เคยเห็น
🇨🇳 中文
损失降到了几乎为零,我们盯着的那一条消息也答对了。但这两件事都证明不了多少。损失是在训练样例上量的,而这些样例模型已经见过了;一条消息也只是一条消息。老实的问法是:对它从没见过的消息,模型会怎么做。
10 Test both models on messages they never saw · ทดสอบโมเดลทั้งสองตัวกับข้อความที่มันไม่เคยเห็น · 用它们没见过的消息测试两个模型
🇬🇧 English
score.py loads a trained model and runs both tests. Its function load_tuned shows the practical difference between the two results once more. The full model is loaded from its own folder, like any model. The LoRA model is loaded in two moves: first the model you downloaded, then PeftModel.from_pretrained places the add-ons from tuned_lora beside it.
🇹🇭 ไทย
score.py โหลดโมเดลที่ฝึกแล้ว และรันชุดทดสอบทั้งสองชุด ฟังก์ชัน load_tuned ของมันแสดงความต่างในทางปฏิบัติระหว่างผลลัพธ์สองแบบอีกครั้ง โมเดลแบบเต็มโหลดจากโฟลเดอร์ของมันเอง เหมือนโมเดลทั่วไป ส่วนโมเดลแบบ LoRA โหลดเป็นสองจังหวะ คือโหลดโมเดลที่คุณดาวน์โหลดมาก่อน แล้ว PeftModel.from_pretrained จึงวางตัวเสริมจาก tuned_lora ไว้ข้างมัน
🇨🇳 中文
score.py 加载一个训练好的模型,并运行两份测试。它的函数 load_tuned 再一次显示了两种成果在实际使用上的差别。全量训练的模型从它自己的文件夹加载,和任何模型一样。LoRA 的模型分两步加载:先加载你下载的那个模型,再由 PeftModel.from_pretrained 把 tuned_lora 里的附加模块放到它旁边。
💻 RUN IT
venv\Scripts\python score.py lora
venv\Scripts\python score.py full
📄 NEW FILE · score.py
"""score.py - test a fine-tuned model on messages it has never seen.
python score.py lora
python score.py full
It uses two files. test_generated.jsonl was written by the same program as the
training examples. test_handwritten.jsonl was written by a person. It prints
every order the model gets wrong, and then how many it got right.
"""
import json
import sys
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
INSTRUCTION = "You take orders for a noodle shop. Reply with the order as JSON."
def read_examples(name):
"""Every line of the file is one example, written as JSON."""
examples = []
with open(name, encoding="utf-8") as file:
for line in file:
examples.append(json.loads(line))
return examples
def load_tuned(kind):
"""The model after training: a whole new model, or the old one plus its add-ons."""
if kind == "full":
return AutoModelForCausalLM.from_pretrained("tuned_full")
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32)
return PeftModel.from_pretrained(model, "tuned_lora")
def reply_to(model, tokenizer, message):
"""The model's reply to one message, on one line."""
chat = [
{"role": "system", "content": INSTRUCTION},
{"role": "user", "content": message},
]
chat_tokens = tokenizer.apply_chat_template(
chat, add_generation_prompt=True, return_tensors="pt")
question_tokens = chat_tokens["input_ids"]
output = model.generate(question_tokens, max_new_tokens=50, do_sample=False)
# output is the question and then the reply. Keep the reply.
reply_tokens = output[0][question_tokens.shape[1]:]
reply = tokenizer.decode(reply_tokens, skip_special_tokens=True)
return " ".join(reply.split())
def is_right(reply, order):
"""True when the reply is JSON and says exactly what the order says."""
try:
return json.loads(reply) == order
except ValueError: # json.loads could not read the reply as JSON
return False
def main():
kind = sys.argv[1] # the word after score.py: "lora" or "full"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = load_tuned(kind)
for name in ["test_generated.jsonl", "test_handwritten.jsonl"]:
tests = read_examples(name)
right = 0
for test in tests:
reply = reply_to(model, tokenizer, test["message"])
if is_right(reply, test["order"]):
right = right + 1
else:
print(" ", test["message"])
print(" wanted", json.dumps(test["order"]))
print(" got ", reply)
print(f"{name}: {right} of {len(tests)} right")
print()
if __name__ == "__main__":
main()
Orders right, out of the 50 handwritten test messagesออร์เดอร์ที่ตอบถูก จากข้อความ 50 ข้อในชุดทดสอบที่คนเขียนเอง50 条手写的测试消息里,答对了多少条订单
The four numbers are read from the outputs of before.py and score.py on this page.ตัวเลขทั้งสี่ตัวอ่านมาจากผลลัพธ์ของ before.py และ score.py ในหน้านี้这四个数字是从本页 before.py 和 score.py 的输出里读出来的。
Show what the LoRA model got wrong · แสดงข้อที่โมเดล LoRA ตอบผิด · 显示 LoRA 模型答错的那些
hi, two pad thai with pork, for here, no spice. thank you!
wanted {"dish": "pad thai", "protein": "pork", "spice": "none", "count": 2, "takeaway": false}
got {"dish": "pad thai", "protein": "pork", "spice": "none", "count": 2, "takeaway": true}
test_generated.jsonl: 49 of 50 right
four khao soi beef hot to go
wanted {"dish": "khao soi", "protein": "beef", "spice": "hot", "count": 4, "takeaway": true}
got {"dish": "khao soi", "protein": "beef", "spice": "hot", "count": 4, "takeaway": false}
2 pad thai chicken no chilli to go please
wanted {"dish": "pad thai", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
got {"dish": "pad thai", "protein": "chicken", "spice": "medium", "count": 2, "takeaway": true}
I'd love a bowl of nam ngiao with pork, as spicy as you can make it! I'm sitting at table 4.
wanted {"dish": "nam ngiao", "protein": "pork", "spice": "hot", "count": 1, "takeaway": false}
got {"dish": "nam ngiao", "protein": "pork", "spice": "hot", "count": 4, "takeaway": false}
khao soi x2, tofu, little bit spicy, take home
wanted {"dish": "khao soi", "protein": "tofu", "spice": "mild", "count": 2, "takeaway": true}
got {"dish": "khao soi", "protein": "tofu", "spice": "little bit", "count": 2, "takeaway": true}
Just one pad see ew with pork for me.
wanted {"dish": "pad see ew", "protein": "pork", "spice": "medium", "count": 1, "takeaway": false}
got {"dish": "pad see ew", "protein": "pork", "spice": "medium", "count": 1, "takeaway": true}
A couple of nam ngiao with beef, extra hot, for here.
wanted {"dish": "nam ngiao", "protein": "beef", "spice": "hot", "count": 2, "takeaway": false}
got {"dish": "nam ngiao", "protein": "beef", "spice": "hot", "count": 1, "takeaway": false}
Please pack 2 pad see ew with chicken. My kids will eat them, so not spicy.
wanted {"dish": "pad see ew", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
got {"dish": "pad see ew", "protein": "chicken", "spice": "none", "count": 2, "takeaway": false}
We are three people and each of us wants khao soi with pork, normal spicy, and we will eat at the shop.
wanted {"dish": "khao soi", "protein": "pork", "spice": "medium", "count": 3, "takeaway": false}
got {"dish": "khao soi", "protein": "pork", "spice": "normal", "count": 3, "takeaway": false}
kao soi with chiken, 1 bowl, medium
wanted {"dish": "khao soi", "protein": "chicken", "spice": "medium", "count": 1, "takeaway": false}
got {"dish": "kao soi", "protein": "chimen", "spice": "medium", "count": 1, "takeaway": false}
Yesterday's khao soi was great. Today I want one pad thai with beef, hot, to take away.
wanted {"dish": "pad thai", "protein": "beef", "spice": "hot", "count": 1, "takeaway": true}
got {"dish": "khao soi", "protein": "beef", "spice": "hot", "count": 1, "takeaway": true}
Is it possible to get khao soi with tofu instead of chicken? One bowl, medium, for here.
wanted {"dish": "khao soi", "protein": "tofu", "spice": "medium", "count": 1, "takeaway": false}
got {"dish": "khao soi", "protein": "tofu", "spice": "medium", "count": 1, "takeaway": true}
test_handwritten.jsonl: 39 of 50 right
Show what the fully trained model got wrong · แสดงข้อที่โมเดลที่ฝึกแบบเต็มตอบผิด · 显示全量训练的模型答错的那些
test_generated.jsonl: 50 of 50 right
four khao soi beef hot to go
wanted {"dish": "khao soi", "protein": "beef", "spice": "hot", "count": 4, "takeaway": true}
got {"dish": "khao soi", "protein": "beef", "spice": "hot", "count": 4, "takeaway": false}
2 pad thai chicken no chilli to go please
wanted {"dish": "pad thai", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
got {"dish": "pad thai", "protein": "chicken", "spice": "hot", "count": 2, "takeaway": true}
I'd love a bowl of nam ngiao with pork, as spicy as you can make it! I'm sitting at table 4.
wanted {"dish": "nam ngiao", "protein": "pork", "spice": "hot", "count": 1, "takeaway": false}
got {"dish": "nam ngiao", "protein": "pork", "spice": "hot", "count": 4, "takeaway": false}
My friend and I each want a khao soi with chicken. Medium is fine. We are eating in.
wanted {"dish": "khao soi", "protein": "chicken", "spice": "medium", "count": 2, "takeaway": false}
got {"dish": "khao soi", "protein": "chicken", "spice": "medium", "count": 1, "takeaway": false}
Please pack 2 pad see ew with chicken. My kids will eat them, so not spicy.
wanted {"dish": "pad see ew", "protein": "chicken", "spice": "none", "count": 2, "takeaway": true}
got {"dish": "pad see ew", "protein": "chicken", "spice": "none", "count": 2, "takeaway": false}
We are three people and each of us wants khao soi with pork, normal spicy, and we will eat at the shop.
wanted {"dish": "khao soi", "protein": "pork", "spice": "medium", "count": 3, "takeaway": false}
got {"dish": "khao soi", "protein": "pork", "spice": "normal", "count": 3, "takeaway": false}
kao soi with chiken, 1 bowl, medium
wanted {"dish": "khao soi", "protein": "chicken", "spice": "medium", "count": 1, "takeaway": false}
got {"dish": "kao soi", "protein": "chiken", "spice": "medium", "count": 1, "takeaway": false}
Do you have nam ngiao? If yes, two with pork, please. Spice level: hot. I'll take them with me.
wanted {"dish": "nam ngiao", "protein": "pork", "spice": "hot", "count": 2, "takeaway": true}
got {"dish": "nam ngiao", "protein": "pork", "spice": "hot", "count": 2, "takeaway": false}
I am vegetarian, so tofu please. One pad see ew, not too hot.
wanted {"dish": "pad see ew", "protein": "tofu", "spice": "mild", "count": 1, "takeaway": false}
got {"dish": "pad see ew", "protein": "tofu", "spice": "hot", "count": 1, "takeaway": false}
One more nam ngiao with chicken for table 2, please, and make it mild.
wanted {"dish": "nam ngiao", "protein": "chicken", "spice": "mild", "count": 1, "takeaway": false}
got {"dish": "nam ngiao", "protein": "chicken", "spice": "mild", "count": 2, "takeaway": false}
One nam ngiao with tofu. Please don't make it spicy, and put it in a bag for me.
wanted {"dish": "nam ngiao", "protein": "tofu", "spice": "none", "count": 1, "takeaway": true}
got {"dish": "nam ngiao", "protein": "tofu", "spice": "none", "count": 1, "takeaway": false}
Pad thai with beef for one person, mild, and I'm taking it back to my hotel.
wanted {"dish": "pad thai", "protein": "beef", "spice": "mild", "count": 1, "takeaway": true}
got {"dish": "pad thai", "protein": "beef", "spice": "mild", "count": 1, "takeaway": false}
test_handwritten.jsonl: 38 of 50 right
🇬🇧 English
On the generated test both models are nearly perfect: 49 and 50 of 50. They learned what the examples showed. On the handwritten test they get 39 and 38. That is a long way from the 1 of 50 that the best prompt managed. It also means that 11 or 12 customers in 50 would get the wrong order.
Open the two lists and read the misses. They fall into a few kinds, and each kind points back at the training examples.
- Words the examples never used.
Please pack 2 pad see ew
is a takeaway order, but no training message saidpack
.A couple of
was never a number. Forlittle bit spicy
the LoRA model wrote"spice": "little bit", and fornormal spicy
both models wrote"normal". Faced with a phrase it had not met, the model copied the customer's words into the form, though the form allows only four answers. - Misspellings.
kao soi with chiken
came back with the dishkao soi. Every training message was spelled correctly, so the model learned to copy the name of the dish, not to recognise it. - Numbers that are not the order.
I'm sitting at table 4
became four bowls, from both models. In the training messages a number always meant how many. - A second dish in the message.
Yesterday's khao soi was great. Today I want one pad thai
came back from the LoRA model as khao soi. No training message named two dishes.
None of these is a fault in the training loop. None would be fixed by training for longer, or by choosing the full way instead of LoRA: both models make the same kinds of mistake. They would be fixed by better examples. That is the main thing to take from this page. The model became exactly as good as its examples, and no better.
🇹🇭 ไทย
ในชุดทดสอบที่โปรแกรมสร้าง โมเดลทั้งสองตัวเกือบสมบูรณ์แบบ: 49 และ 50 จาก 50 มันเรียนรู้สิ่งที่ตัวอย่างแสดงให้เห็นแล้ว ในชุดทดสอบที่คนเขียนเอง มันได้ 39 และ 38 ซึ่งห่างไกลจาก 1 จาก 50 ที่พรอมต์ที่ดีที่สุดทำได้ แต่ก็หมายความด้วยว่า ลูกค้า 11 หรือ 12 คนจาก 50 คนจะได้ออร์เดอร์ผิด
เปิดรายการทั้งสองแล้วอ่านข้อที่ผิด มันแบ่งได้เป็นไม่กี่ประเภท และแต่ละประเภทชี้กลับไปที่ตัวอย่างที่ใช้ฝึก
- คำที่ตัวอย่างไม่เคยใช้
Please pack 2 pad see ew
เป็นออร์เดอร์แบบซื้อกลับ แต่ไม่มีข้อความฝึกข้อไหนพูดว่าpack
ส่วนA couple of
ก็ไม่เคยเป็นจำนวน สำหรับlittle bit spicy
โมเดล LoRA เขียน"spice": "little bit"และสำหรับnormal spicy
โมเดลทั้งสองตัวเขียน"normal"เมื่อเจอวลีที่ไม่เคยเห็น โมเดลลอกคำของลูกค้าลงไปในแบบฟอร์ม ทั้งที่แบบฟอร์มอนุญาตคำตอบแค่สี่ค่า - คำที่สะกดผิด
kao soi with chiken
กลับมาพร้อมชื่ออาหารว่าkao soiข้อความฝึกทุกข้อสะกดถูกหมด โมเดลจึงเรียนรู้ที่จะลอกชื่ออาหาร ไม่ใช่รู้จักมัน - ตัวเลขที่ไม่ใช่ออร์เดอร์
I'm sitting at table 4
กลายเป็นสี่ชาม จากโมเดลทั้งสองตัว ในข้อความฝึก ตัวเลขหมายถึงจำนวนที่สั่งเสมอ - อาหารอย่างที่สองในข้อความ
Yesterday's khao soi was great. Today I want one pad thai
กลับมาจากโมเดล LoRA เป็น khao soi ไม่มีข้อความฝึกข้อไหนเอ่ยถึงอาหารสองอย่าง
ไม่มีข้อไหนในนี้เป็นความบกพร่องของลูปการฝึก และไม่มีข้อไหนแก้ได้ด้วยการฝึกให้นานขึ้น หรือด้วยการเลือกแบบเต็มแทน LoRA เพราะโมเดลทั้งสองตัวผิดแบบเดียวกัน สิ่งที่จะแก้ได้คือตัวอย่างที่ดีขึ้น นี่คือสิ่งสำคัญที่สุดที่ควรได้จากหน้านี้ โมเดลเก่งเท่ากับตัวอย่างของมันพอดี และไม่เก่งไปกว่านั้น
🇨🇳 中文
在生成的测试上,两个模型几乎满分:50 条里对了 49 条和 50 条。它们学会了样例所展示的东西。在手写的测试上,它们对了 39 条和 38 条。这比最好的提示词做到的 1 条强得多;但这也意味着,50 位顾客里会有 11 或 12 位拿到错的订单。
打开那两份清单,读一读答错的那些。它们可以归成几类,而每一类都指回训练样例。
- 样例里从没用过的词。
Please pack 2 pad see ew
是一份外带订单,可是没有哪条训练消息说过pack
。A couple of
也从来不是一个数量。对little bit spicy
,LoRA 模型写了"spice": "little bit";对normal spicy
,两个模型都写了"normal"。遇到没见过的短语,模型就把顾客的原话抄进表单,尽管表单只允许四个答案。 - 拼写错误。
kao soi with chiken
得到的菜名是kao soi。每条训练消息都拼写正确,所以模型学会的是照抄菜名,而不是认出它。 - 不属于订单的数字。
I'm sitting at table 4
在两个模型那里都变成了四碗。在训练消息里,数字永远表示点了多少份。 - 消息里出现了第二道菜。
Yesterday's khao soi was great. Today I want one pad thai
在 LoRA 模型那里变成了 khao soi。没有哪条训练消息提到过两道菜。
这些都不是训练循环的毛病。训练得更久也修不好,把 LoRA 换成全量的方式也修不好:两个模型犯的是同一类错误。能修好它们的是更好的样例。这是这一页最该带走的一点:模型恰好和它的样例一样好,不会更好。
11 Use it inside a program · เอาไปใช้ในโปรแกรม · 在程序里使用它
🇬🇧 English
A model that turns messages into JSON is only useful if a program then does something with the JSON. use.py is that program in miniature: it works out what each order costs.
Two things in it are new. The first is merge_and_unload(). While you are still training and testing, it is convenient to keep the add-ons beside the model. When you come to use the model, you can add them into the model's own numbers once and for all. The result gives the same replies, faster: on our laptop 1.26 seconds a reply instead of 1.83, with the same reply to all ten messages we tried.
The second is read_order, and it matters more. A model's reply is a guess, even after training. Before the program acts on a reply, read_order checks that it is JSON, that it is a dictionary, and that each of the five answers is one the shop allows. For anything else it returns None, and the program hands the message to a person.
🇹🇭 ไทย
โมเดลที่แปลงข้อความเป็น JSON จะมีประโยชน์ก็ต่อเมื่อมีโปรแกรมเอา JSON นั้นไปทำอะไรต่อ use.py คือโปรแกรมแบบนั้นในขนาดจิ๋ว มันคำนวณว่าออร์เดอร์แต่ละรายการราคาเท่าไร
มีของใหม่สองอย่างในไฟล์นี้ อย่างแรกคือ merge_and_unload() ระหว่างที่คุณยังฝึกและทดสอบอยู่ การเก็บตัวเสริมไว้ข้างโมเดลนั้นสะดวก แต่เมื่อถึงเวลาใช้งาน คุณรวมมันเข้าไปในตัวเลขของโมเดลเองได้เลยทีเดียวจบ ผลที่ได้ให้คำตอบเหมือนเดิมและเร็วขึ้น บนแล็ปท็อปของเราคือ 1.26 วินาทีต่อคำตอบ แทนที่จะเป็น 1.83 โดยได้คำตอบเดียวกันครบทั้งสิบข้อความที่เราลอง
อย่างที่สองคือ read_order และมันสำคัญกว่า คำตอบของโมเดลคือการเดา แม้จะผ่านการฝึกมาแล้วก็ตาม ก่อนที่โปรแกรมจะทำอะไรตามคำตอบ read_order ตรวจว่ามันเป็น JSON เป็นดิกชันนารี และคำตอบทั้งห้าค่าเป็นค่าที่ร้านอนุญาต ถ้าไม่ใช่ มันคืน None และโปรแกรมก็ส่งข้อความนั้นให้คนจัดการ
🇨🇳 中文
一个把消息变成 JSON 的模型,只有当程序接着拿这段 JSON 去做点什么时才有用。use.py 就是这样一个微型的程序:它算出每份订单要多少钱。
里面有两样新东西。第一样是 merge_and_unload()。还在训练和测试的时候,把附加模块放在模型旁边比较方便。到了真正使用模型的时候,可以把它们一次性并进模型自己的数字里。这样得到的回复相同,而且更快:在我们的笔记本上,每条回复 1.26 秒,而不是 1.83 秒,我们试的十条消息回复全都一样。
第二样是 read_order,它更要紧。模型的回复是一种猜测,训练之后也一样。在程序按回复行事之前,read_order 先检查:它是不是 JSON,是不是一个字典,五个答案是不是都是店里允许的。只要有一样不符,它就返回 None,程序就把这条消息交给人来处理。
💻 RUN IT
venv\Scripts\python use.py
📄 NEW FILE · use.py
"""use.py - use the fine-tuned model inside an ordinary program.
python use.py
It needs the folder tuned_lora, which "python train.py lora" makes.
"""
import json
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "HuggingFaceTB/SmolLM2-135M-Instruct"
INSTRUCTION = "You take orders for a noodle shop. Reply with the order as JSON."
PRICES = {"khao soi": 60, "nam ngiao": 50, "pad thai": 60, "pad see ew": 55} # baht
PROTEINS = ["chicken", "pork", "beef", "tofu"]
SPICES = ["none", "mild", "medium", "hot"]
MESSAGES = [
"Hi, two khao soi with beef, very spicy, to go please.",
"one pad thai tofu not spicy",
"3 nam ngiao with pork, a tiny bit spicy, eat in",
"What time do you close today?",
]
def reply_to(model, tokenizer, message):
"""The model's reply to one message, on one line."""
chat = [
{"role": "system", "content": INSTRUCTION},
{"role": "user", "content": message},
]
chat_tokens = tokenizer.apply_chat_template(
chat, add_generation_prompt=True, return_tensors="pt")
question_tokens = chat_tokens["input_ids"]
output = model.generate(question_tokens, max_new_tokens=50, do_sample=False)
# output is the question and then the reply. Keep the reply.
reply_tokens = output[0][question_tokens.shape[1]:]
reply = tokenizer.decode(reply_tokens, skip_special_tokens=True)
return " ".join(reply.split())
def read_order(reply):
"""The order in a reply, as a dictionary. None if the reply is not a proper order.
A model's reply is a guess. Check it before the program acts on it.
"""
try:
order = json.loads(reply)
except ValueError: # the reply is not JSON
return None
if not isinstance(order, dict):
return None
if order.get("dish") not in PRICES:
return None
if order.get("protein") not in PROTEINS:
return None
if order.get("spice") not in SPICES:
return None
if order.get("count") not in [1, 2, 3, 4, 5, 6]:
return None
if order.get("takeaway") not in [True, False]:
return None
return order
def main():
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float32)
model = PeftModel.from_pretrained(model, "tuned_lora")
model = model.merge_and_unload() # fold the add-ons into the model's own numbers
for message in MESSAGES:
print(message)
reply = reply_to(model, tokenizer, message)
order = read_order(reply)
if order is None:
print(" NOT A PROPER ORDER, ask a person:", reply)
continue
price = PRICES[order["dish"]] * order["count"]
if order["takeaway"]:
place = "takeaway"
else:
place = "eat in"
print(f" {order['count']} x {order['dish']}, {order['protein']},"
f" {order['spice']}, {place}: {price} baht")
if __name__ == "__main__":
main()
🖨️ WHAT IT PRINTED
Hi, two khao soi with beef, very spicy, to go please.
2 x khao soi, beef, hot, takeaway: 120 baht
one pad thai tofu not spicy
1 x pad thai, tofu, none, eat in: 60 baht
3 nam ngiao with pork, a tiny bit spicy, eat in
3 x nam ngiao, pork, mild, eat in: 150 baht
What time do you close today?
NOT A PROPER ORDER, ask a person: {"dish": "noodle", "protein": "spam", "spice": "medium", "count": 1, "takeaway": false}
🇬🇧 English
The first three orders are right, and that includes a tiny bit spicy
, a phrase the model had never seen. Then comes a message that is not an order at all: What time do you close today?
The model answered it with an order for one noodle with spam.
It is easy to laugh at, and important to understand. Every one of the 400 training examples was an order, so the model learned that whatever a customer writes, the reply is an order. It has no way to say this is not an order
, because no example ever showed it one. read_order caught this reply, since the shop sells neither of those things. It will not always be so lucky. The model could just as well have invented one khao soi with chicken, and that would have passed. If your program will meet messages that are not orders, then some of your training examples must be messages that are not orders, with the answer you want for them.
We asked the three models, the one you downloaded and the two trained ones, a few more things that are not orders. Without the shop's instruction, all three still answered a question about Thailand and wrote a sentence about elephants: the training did not wipe out what the model knew. With the shop's instruction, both trained models turned the question about closing time into an order, and the fully trained one also turned What is the capital of Thailand?
into an order, where the LoRA one still answered Bangkok. Three questions are an illustration and not a measurement, so do not read a rule into that last difference.
🇹🇭 ไทย
ออร์เดอร์สามรายการแรกถูก รวมทั้ง a tiny bit spicy
ซึ่งเป็นวลีที่โมเดลไม่เคยเห็น แล้วก็มาถึงข้อความที่ไม่ใช่ออร์เดอร์เลย: What time do you close today?
โมเดลตอบด้วยออร์เดอร์ noodle หนึ่งที่ ใส่ spam
มันน่าขำ แต่ก็สำคัญที่ต้องเข้าใจ ตัวอย่างสำหรับฝึกทั้ง 400 ชุดเป็นออร์เดอร์ทั้งหมด โมเดลจึงเรียนรู้ว่า ไม่ว่าลูกค้าจะเขียนอะไรมา คำตอบก็คือออร์เดอร์ มันไม่มีทางบอกได้ว่า นี่ไม่ใช่ออร์เดอร์
เพราะไม่เคยมีตัวอย่างไหนแสดงให้มันเห็น read_order จับคำตอบนี้ได้ เพราะร้านไม่ได้ขายของทั้งสองอย่างนั้น แต่มันจะไม่โชคดีแบบนี้ทุกครั้ง โมเดลอาจแต่งเป็น khao soi ใส่ไก่หนึ่งที่ก็ได้ และแบบนั้นจะผ่านการตรวจ ถ้าโปรแกรมของคุณจะต้องเจอข้อความที่ไม่ใช่ออร์เดอร์ ตัวอย่างสำหรับฝึกบางส่วนของคุณก็ต้องเป็นข้อความที่ไม่ใช่ออร์เดอร์ พร้อมคำตอบที่คุณต้องการสำหรับข้อความเหล่านั้น
เราถามโมเดลทั้งสามตัว คือตัวที่คุณดาวน์โหลดมากับตัวที่ฝึกแล้วอีกสองตัว ด้วยคำถามอีกสองสามข้อที่ไม่ใช่ออร์เดอร์ เมื่อไม่มีคำสั่งของร้าน ทั้งสามตัวยังตอบคำถามเรื่องประเทศไทยได้ และเขียนประโยคเกี่ยวกับช้างได้ การฝึกไม่ได้ลบสิ่งที่โมเดลรู้อยู่ทิ้งไป เมื่อมีคำสั่งของร้าน โมเดลที่ฝึกแล้วทั้งสองตัวเปลี่ยนคำถามเรื่องเวลาปิดร้านเป็นออร์เดอร์ และตัวที่ฝึกแบบเต็มยังเปลี่ยน What is the capital of Thailand?
เป็นออร์เดอร์ด้วย ขณะที่ตัว LoRA ยังตอบว่า Bangkok คำถามสามข้อเป็นแค่ตัวอย่างให้เห็นภาพ ไม่ใช่การวัด จึงอย่าเอาความต่างข้อสุดท้ายนั้นไปสรุปเป็นกฎ
🇨🇳 中文
前三份订单都对,其中包括 a tiny bit spicy
,一个模型从没见过的短语。接着来了一条根本不是订单的消息:What time do you close today?
模型的回答是一份订单:一份 noodle,配 spam。
这很好笑,但也很值得弄明白。400 个训练样例每一个都是订单,所以模型学到的是:不管顾客写什么,回复都是一份订单。它没有办法说这不是订单
,因为没有哪个样例给它看过这种情况。read_order 拦下了这条回复,因为店里不卖这两样东西。但它不会每次都这么走运。模型完全可能编出一份鸡肉 khao soi,那就会通过检查。如果你的程序会遇到不是订单的消息,那么你的训练样例里就必须有一些不是订单的消息,并配上你希望得到的回答。
我们又问了三个模型(你下载的那个,以及训练出来的两个)几件不是订单的事。不带店里的指令时,三个模型都还能回答关于泰国的问题,也能写出一句关于大象的话:训练并没有抹掉模型原本知道的东西。带上店里的指令时,两个训练过的模型都把关于打烊时间的问题变成了订单;全量训练的那个还把 What is the capital of Thailand?
也变成了订单,而 LoRA 的那个仍然回答 Bangkok。三个问题只是一个示意,不是一次测量,所以不要从最后这个差别里读出什么规律。
Show the three questions and every reply · แสดงคำถามสามข้อและคำตอบทั้งหมด · 显示这三个问题和所有的回复
What is the capital of Thailand?
no instruction before The capital of Thailand is Bangkok.
no instruction lora The capital of Thailand is Bangkok. It is a vibrant city known for its rich history, cultural richness, and global appeal. Bangkok is also the largest city in Thailand, with a population of
no instruction full The capital of Thailand is Bangkok.
shop instruction before The capital of Thailand is Bangkok.
shop instruction lora The capital of Thailand is Bangkok.
shop instruction full {"dish": "khao noodle", "protein": "chicken", "spice": "medium", "count": 1, "takeaway": false}
Write one short sentence about elephants.
no instruction before Elephants are majestic creatures with a heart full of life and a mind full of intelligence.
no instruction lora An enthusiastic and curious animal, elephants are known for their massive size, powerful legs, and unique social behavior.
no instruction full Elephants are majestic creatures with a heart full of life and a mind full of intelligence.
shop instruction before Elephants are intelligent, social, and highly social animals that live in large herds. They are highly social animals and live in large herds, which can number in the hundreds. They are highly social animals
shop instruction lora A majestic and social animal, the elephant is known for its long, slender body, powerful legs, and distinctive trunk-like appendage.
shop instruction full Elephants are massive, intelligent creatures with a long, curved trunk and a thick, thick neck. They are known for their intelligence, social behavior, and impressive memory.
What time do you close today?
no instruction before I'm sorry for any misunderstanding, but as a chatbot, I don't have the ability to access or control the time. I'm designed to provide information and assistance, not to provide specific time
no instruction lora "SmolLM" is a helpful AI designed to provide information about the current time.
no instruction full "Hughes, I'm closed today at 10:00 PM. Please check your calendar for any updates or special events."
shop instruction before I close my restaurant at 10:00 PM.
shop instruction lora {"dish": "noodle", "protein": "spam", "spice": "medium", "count": 1, "takeaway": false}
shop instruction full {"dish": "noodle", "protein": "chicken", "spice": "medium", "count": 1, "takeaway": false}
🖥️ Tutorial 5 — A larger model, on a GPU · บทที่ 5 — โมเดลที่ใหญ่กว่า บนการ์ดจอ · 教程 5 —— 更大的模型,用 GPU 来训练
🇬🇧 English
Everything above ran on a laptop because the model is tiny. The same two ideas, full fine-tuning and LoRA, are how much larger models are adapted, and there the hardware decides what is possible. This part looks at one real library that trains on a GPU.
We could not run GPU training on a laptop. So in this part the page reports what the library's own code and documentation say. Each fact comes from a script that reads the library and prints what it finds, and you can run the same script.
One library this tutorial leaves out is LlamaIndex: its fine-tuning does not run on your computer but on the servers of other companies, such as OpenAI and Mistral.
🇹🇭 ไทย
ทุกอย่างข้างบนรันบนแล็ปท็อปได้เพราะโมเดลเล็กจิ๋ว แนวคิดสองข้อเดียวกันนี้ คือการปรับจูนแบบเต็มและ LoRA ก็เป็นวิธีที่ใช้ปรับโมเดลที่ใหญ่กว่านี้มาก และตรงนั้นฮาร์ดแวร์เป็นตัวตัดสินว่าอะไรทำได้ ส่วนนี้จะดูไลบรารีจริงตัวหนึ่งที่ฝึกบนการ์ดจอ
เราฝึกบนการ์ดจอด้วยแล็ปท็อปไม่ได้ ในส่วนนี้หน้านี้จึงรายงานสิ่งที่โค้ดและเอกสารของไลบรารีเองบอกไว้ ข้อเท็จจริงแต่ละข้อมาจากสคริปต์ที่อ่านไลบรารีแล้วพิมพ์สิ่งที่พบออกมา และคุณรันสคริปต์เดียวกันนี้ได้
ไลบรารีหนึ่งที่บทเรียนนี้ไม่ครอบคลุมคือ LlamaIndex เพราะการปรับจูนของมันไม่ได้รันบนคอมพิวเตอร์ของคุณ แต่รันบนเซิร์ฟเวอร์ของบริษัทอื่น เช่น OpenAI และ Mistral
🇨🇳 中文
上面的一切都能在笔记本上运行,是因为模型非常小。同样的两个想法,全量微调和 LoRA,也是人们调整大得多的模型的办法;到了那里,能做什么就由硬件决定了。这一部分来看一个真正在 GPU 上训练的库。
我们没法在笔记本上做 GPU 训练。所以在这一部分,页面报告的是这个库自己的代码和文档怎么说。每一条事实都来自一个脚本,它读这个库,把读到的东西打印出来;你也可以运行同一个脚本。
有一个库这篇教程没有讲,那就是 LlamaIndex:它的微调不在你的电脑上运行,而是在别的公司(比如 OpenAI 和 Mistral)的服务器上运行。
12 A full fine-tune on a GPU: how decider was made · การปรับจูนแบบเต็มบนการ์ดจอ: decider ถูกสร้างมาอย่างไร · 在 GPU 上做全量微调:decider 是怎么做出来的
🇬🇧 English
decider is the model behind the hunters in our pond game, and it has a page of its own. It does not write text. It reads a situation and a question with fixed choices, and gives the probability of each choice. It is also a fine-tuned model, and its library, decider-ai, ships the script that fine-tuned it. That makes it a good look at what the full way means at real size.
decider-2b began as Qwen3.5-2B-Base, a general language model of about 1.9 billion numbers, fourteen times the size of the model on this page. The library's train.py trains it the full way, and one line proves it: opt = torch.optim.AdamW(model.parameters(), ...). That is the same optimizer as in our own train.py, and it is handed every number in the model. Its learning rate is 1e-5, which is 0.00001: half of the small rate we used for the full way.
Another line says where it runs: model = DecisionModel(a.model).cuda(). .cuda() moves the model onto an NVIDIA graphics card, and the script offers no other way. On our laptop PyTorch finds no such card, so this script cannot even start there. The library's README gives the size of the job on the hardware it was made for. One pass over its training data is 1.47 million examples, and takes 5.3 hours on a GH200, a data-centre machine from NVIDIA, with another 45 minutes for testing. The first lines of scripts/train.sh say what it expects: One GH200 / H100-class GPU
. Compare that with step 7: 400 examples, and 431 seconds on a laptop.
If you have such a machine, these are the README's own commands. They are meant to be run inside a copy of the library's repository from GitHub, because the scripts folder is there and is not part of what pip installs.
🇹🇭 ไทย
decider คือโมเดลที่อยู่เบื้องหลังนักล่าในเกมบ่อปลาของเรา และมันมีหน้าของตัวเอง มันไม่ได้เขียนข้อความ มันอ่านสถานการณ์กับคำถามที่มีตัวเลือกตายตัว แล้วให้ความน่าจะเป็นของแต่ละตัวเลือก มันเป็นโมเดลที่ผ่านการปรับจูนมาเช่นกัน และไลบรารีของมัน คือ decider-ai มาพร้อมสคริปต์ที่ใช้ปรับจูนมัน จึงเป็นตัวอย่างที่ดีให้เห็นว่าแบบเต็มหมายถึงอะไรในขนาดจริง
decider-2b เริ่มต้นจาก Qwen3.5-2B-Base โมเดลภาษาทั่วไปที่มีตัวเลขราว 1.9 พันล้านตัว ใหญ่กว่าโมเดลในหน้านี้สิบสี่เท่า train.py ของไลบรารีฝึกมันแบบเต็ม และมีบรรทัดหนึ่งที่พิสูจน์เรื่องนี้: opt = torch.optim.AdamW(model.parameters(), ...) นี่คือ optimizer ตัวเดียวกับใน train.py ของเราเอง และมันได้รับตัวเลขทุกตัวในโมเดลไป learning rate ของมันคือ 1e-5 ซึ่งก็คือ 0.00001 เป็นครึ่งหนึ่งของค่าเล็กที่เราใช้กับแบบเต็ม
อีกบรรทัดหนึ่งบอกว่ามันรันที่ไหน: model = DecisionModel(a.model).cuda() โดย .cuda() ย้ายโมเดลไปไว้บนการ์ดจอ NVIDIA และสคริปต์ไม่มีทางอื่นให้เลือก บนแล็ปท็อปของเรา PyTorch หาการ์ดจอแบบนั้นไม่เจอ สคริปต์นี้จึงเริ่มทำงานที่นั่นไม่ได้ด้วยซ้ำ README ของไลบรารีบอกขนาดของงานบนฮาร์ดแวร์ที่มันถูกสร้างมาให้ใช้ การผ่านข้อมูลฝึกหนึ่งรอบคือตัวอย่าง 1.47 ล้านชุด และใช้เวลา 5.3 ชั่วโมงบน GH200 ซึ่งเป็นเครื่องระดับศูนย์ข้อมูลของ NVIDIA กับอีก 45 นาทีสำหรับการทดสอบ บรรทัดแรก ๆ ของ scripts/train.sh บอกสิ่งที่มันต้องการ: One GH200 / H100-class GPU
ลองเทียบกับขั้นที่ 7: ตัวอย่าง 400 ชุด และ 431 วินาทีบนแล็ปท็อป
ถ้าคุณมีเครื่องแบบนั้น นี่คือคำสั่งจาก README เอง คำสั่งเหล่านี้ตั้งใจให้รันในสำเนาของ repository ของไลบรารีจาก GitHub เพราะโฟลเดอร์ scripts อยู่ที่นั่น และไม่ได้เป็นส่วนหนึ่งของสิ่งที่ pip ติดตั้งให้
🇨🇳 中文
decider 是我们池塘游戏里那些猎手背后的模型,它有自己的一页。它不写文字。它读一个情境和一个带有固定选项的问题,然后给出每个选项的概率。它同样是一个微调出来的模型,而它的库 decider-ai 里就带着微调它的那个脚本。所以,要看全量的方式在真实规模下意味着什么,它是个很好的例子。
decider-2b 的起点是 Qwen3.5-2B-Base,一个大约有 19 亿个数字的通用语言模型,是这一页模型的十四倍。库里的 train.py 用全量的方式训练它,有一行代码可以证明:opt = torch.optim.AdamW(model.parameters(), ...)。这和我们自己的 train.py 用的是同一个优化器,而交给它的是模型里的每一个数字。它的学习率是 1e-5,也就是 0.00001:是我们给全量的方式用的那个小学习率的一半。
另一行说明它在哪里运行:model = DecisionModel(a.model).cuda()。.cuda() 把模型搬到 NVIDIA 显卡上,脚本没有提供别的办法。在我们的笔记本上,PyTorch 找不到这样的显卡,所以这个脚本在那里连启动都做不到。库的 README 给出了这项工作在它原本面向的硬件上的规模:把训练数据过一遍是 147 万个样例,在 GH200(NVIDIA 的一种数据中心机器)上要 5.3 小时,测试还要 45 分钟。scripts/train.sh 开头几行说了它需要什么:One GH200 / H100-class GPU
。把这和第 7 步比一比:400 个样例,在笔记本上 431 秒。
如果你有这样的机器,下面就是 README 自己给出的命令。它们要在从 GitHub 取得的这个库的仓库副本里运行,因为 scripts 文件夹在那里,并不在 pip 安装的内容里。
🖥️ ON A GPU · from the library's README · not run by us · จาก README ของไลบรารี · เราไม่ได้รันเอง · 来自这个库的 README · 我们没有运行过
uv venv --python 3.12 .venv312 && uv pip install -p .venv312/bin/python -e ".[serve,train]"
scripts/train.sh full
scripts/train.sh delta runs/some/model
🇬🇧 English
The first line installs the library together with the extra libraries that training needs. scripts/train.sh full downloads about 95 public datasets, builds the training data from them, and trains for one pass, starting from the general model. scripts/train.sh delta starts instead from a decider that already exists and carries on training it, with a slightly smaller learning rate, on new kinds of question mixed with what the README calls a replay sample
of the old data. Showing a model some of its old examples again while it learns new ones is a common way to keep it from forgetting the old job. That is the same weakness you met in step 11, seen from the other side: a model drifts towards whatever it was shown last.
python -m decider.train --help lists every option of the training script. One of them is meant for a first try: --train_cap, which limits the number of examples.
The same family shows the other way too. decider-12b is a much larger model, Gemma-4-12B-it, of 12 billion numbers, and its makers did not train all of them. The README describes it as that model with a merged LoRA (rank 32) trained on 6,000 generated state-tracking decisions and 4,000 replayed rows
. Rank 32 is the r of step 8, and merged
is the merge_and_unload() of step 11. So one project used both: the full way for a model of 2 billion numbers, and LoRA for one of 12 billion.
What the library does not give you is a short path for training a decider on a handful of your own decisions. Its training data is a prepared mixture in its own format, and its README explains how to repeat that training or to continue it, not how to start from a few examples of yours. For most uses you would not want to anyway. A decider is used by writing the question and its choices in plain words, with no training at all, and that is what How decider works is about.
🇹🇭 ไทย
บรรทัดแรกติดตั้งไลบรารีพร้อมไลบรารีเสริมที่การฝึกต้องใช้ scripts/train.sh full ดาวน์โหลดชุดข้อมูลสาธารณะราว 95 ชุด สร้างข้อมูลฝึกจากชุดเหล่านั้น แล้วฝึกหนึ่งรอบโดยเริ่มจากโมเดลทั่วไป ส่วน scripts/train.sh delta เริ่มจาก decider ที่มีอยู่แล้ว และฝึกมันต่อด้วย learning rate ที่เล็กลงเล็กน้อย กับคำถามชนิดใหม่ที่ผสมกับสิ่งที่ README เรียกว่า a replay sample
ของข้อมูลเก่า การให้โมเดลดูตัวอย่างเก่าบางส่วนซ้ำอีกครั้งขณะที่มันเรียนของใหม่ เป็นวิธีที่ใช้กันทั่วไปเพื่อไม่ให้มันลืมงานเก่า นี่คือจุดอ่อนเดียวกับที่คุณเจอในขั้นที่ 11 เพียงแต่มองจากอีกด้านหนึ่ง: โมเดลจะไหลไปหาสิ่งที่มันได้เห็นล่าสุด
python -m decider.train --help แสดงตัวเลือกทุกข้อของสคริปต์ฝึก มีข้อหนึ่งที่ทำไว้สำหรับการลองครั้งแรก คือ --train_cap ซึ่งจำกัดจำนวนตัวอย่าง
ตระกูลเดียวกันนี้ยังแสดงอีกแบบหนึ่งให้เห็นด้วย decider-12b เป็นโมเดลที่ใหญ่กว่ามาก คือ Gemma-4-12B-it ที่มีตัวเลข 12 พันล้านตัว และผู้สร้างไม่ได้ฝึกตัวเลขทั้งหมด README บรรยายว่ามันคือโมเดลตัวนั้น with a merged LoRA (rank 32) trained on 6,000 generated state-tracking decisions and 4,000 replayed rows
โดย rank 32 ก็คือ r ของขั้นที่ 8 และ merged
ก็คือ merge_and_unload() ของขั้นที่ 11 โครงการเดียวจึงใช้ทั้งสองแบบ คือแบบเต็มกับโมเดลที่มีตัวเลข 2 พันล้านตัว และ LoRA กับโมเดลที่มี 12 พันล้านตัว
สิ่งที่ไลบรารีไม่ได้ให้ไว้ คือทางลัดสำหรับฝึก decider ด้วยการตัดสินใจของคุณเองเพียงหยิบมือเดียว ข้อมูลฝึกของมันเป็นส่วนผสมที่เตรียมไว้ในรูปแบบของมันเอง และ README อธิบายวิธีทำการฝึกนั้นซ้ำหรือทำต่อ ไม่ได้อธิบายวิธีเริ่มจากตัวอย่างไม่กี่ชุดของคุณ ซึ่งสำหรับการใช้งานส่วนใหญ่ คุณก็คงไม่อยากทำอยู่แล้ว การใช้ decider คือการเขียนคำถามและตัวเลือกเป็นคำธรรมดา โดยไม่ต้องฝึกอะไรเลย และนั่นคือเรื่องของหน้า decider ทำงานอย่างไร
🇨🇳 中文
第一行安装这个库,连同训练需要的额外的库。scripts/train.sh full 下载大约 95 个公开数据集,用它们做出训练数据,然后从通用模型出发训练一遍。scripts/train.sh delta 则从一个已有的 decider 出发,接着训练它,学习率稍小一些,用的是新种类的问题,再混入 README 所说的旧数据的 a replay sample
(回放样本)。让模型在学新东西的同时再看一些旧样例,是防止它忘掉旧任务的常用办法。这和你在第 11 步遇到的是同一个弱点,只是从另一面看:模型会朝它最后看到的东西漂过去。
python -m decider.train --help 列出训练脚本的每一个选项。其中有一个是为第一次尝试准备的:--train_cap,它限制样例的数量。
同一个家族也展示了另一种方式。decider-12b 是一个大得多的模型,即 Gemma-4-12B-it,有 120 亿个数字,而它的作者并没有训练全部的数字。README 把它描述为这个模型 with a merged LoRA (rank 32) trained on 6,000 generated state-tracking decisions and 4,000 replayed rows
。rank 32 就是第 8 步里的 r,merged
就是第 11 步里的 merge_and_unload()。所以,同一个项目两种方式都用了:对 20 亿个数字的模型用全量的方式,对 120 亿个数字的模型用 LoRA。
这个库没有给你的,是一条用你自己的少量决策来训练 decider 的捷径。它的训练数据是一份按它自己的格式准备好的混合数据,README 讲的是怎样重复那次训练或接着训练,而不是怎样从你的几个样例开始。对大多数用途来说,你本来也不会想这么做。使用 decider 的办法,是用平常的话把问题和选项写出来,完全不需要训练,这正是《decider 是怎么工作的》讲的内容。
Show what our script found in the library · แสดงสิ่งที่สคริปต์ของเราพบในไลบรารี · 显示我们的脚本在这个库里找到了什么
decider-ai 1.8.2, torch 2.14.1+cpu
decider/train.py, 142 lines:
line 56: ap.add_argument("--model", default="Qwen/Qwen3.5-2B-Base")
line 60: ap.add_argument("--lr", type=float, default=1e-5)
line 63: ap.add_argument("--max_tokens", type=int, default=16384, help="tokens per micro-batch (B*T)")
line 88: model = DecisionModel(a.model).cuda()
line 95: opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=a.wd, betas=(0.9, 0.95))
line 115: b = {k: (v.cuda() if torch.is_tensor(v) else v) for k, v in b.items()}
decider/model.py, 69 lines:
line 8: def __init__(self, name, dtype=torch.bfloat16, grad_ckpt=True):
line 15: self.lm.gradient_checkpointing_enable()
From the library's README, about its models:
Gemma-4-12B-it with a merged LoRA (rank 32) trained on 6,000 generated state-tracking decisions and 4,000 replayed rows of
| decider-2b **v11** | Qwen3.5-2B-Base | 1.9B | 32k tokens | 0.752 (regression set) | [Mapika/decider-2b](https://huggingface.co/Mapika/decider-2b) |
| decider-12b **v2** | Gemma-4-12B-it + merged state-tracking LoRA (rank 32) | 12B | 32k tokens | not measured on the regression set; JevBench public
From the library's README, about training:
## Train your own
```bash
uv venv --python 3.12 .venv312 && uv pip install -p .venv312/bin/python -e ".[serve,train]"
scripts/train.sh full # datasets -> data/tasks.pkl -> data/mixture_full.pkl -> one epoch from Qwen3.5-2B-Base -> scripts/evaluate.sh
scripts/train.sh delta runs/some/model # or: continue an existing decider checkpoint on the new formats + a replay sample
```
The full recipe and the data builders are in this repository: `decider/data/` downloads and converts about 95 public datasets
and assembles the mixture (`decider/data/mixture.py` lists every component with its size), `decider/train.py` is the
fine-tune, `scripts/evaluate.sh` scores it. One epoch is 1.47M examples and 455M tokens, 5.3 h on a GH200 plus 45 min of
evaluation, and it reproduces the released supervised weights: it matches v9 on the 94-task set (in-task 0.809 against 0.812,
held-out 0.739 against 0.741) and every probe family within noise, with a fitted temperature of 1.03 instead of 1.36.
The RL stage that turns v8 into v10 ([docs/RL.md](https://github.com/Mapika/decider/blob/main/docs/RL.md)) needs a live Chrome with MiniWoB++, the exact game environments
and the training loop of a separate research repository; it is not in this package yet.
scripts/train.sh, read from https://raw.githubusercontent.com/Mapika/decider/main/scripts/train.sh
line 2: # The reference recipe, end to end. One GH200 / H100-class GPU; PY points at the project venv.
line 3: # scripts/train.sh full Qwen3.5-2B-Base -> decider, one epoch over the full mixture (about 1.3M examples)
line 4: # scripts/train.sh delta M continue from an existing decider checkpoint M on the new formats + a replay sample
line 10: LR=1e-5; [ "$MODE" = delta ] && LR=8e-6
python -m decider.train --help
usage: train.py [-h] [--model MODEL] [--data DATA] --out OUT [--epochs EPOCHS]
[--lr LR] [--wd WD] [--warmup WARMUP]
[--max_tokens MAX_TOKENS] [--accum ACCUM] [--max_ctx MAX_CTX]
[--brier_w BRIER_W] [--label_smooth LABEL_SMOOTH]
[--eval_every EVAL_EVERY] [--eval_limit EVAL_LIMIT]
[--train_cap TRAIN_CAP] [--seed SEED] [--resample_every_epoch]
[--none_prob NONE_PROB]
[--schema_first_prob SCHEMA_FIRST_PROB]
[--max_options MAX_OPTIONS]
options:
-h, --help show this help message and exit
--model MODEL
--data DATA
--out OUT
--epochs EPOCHS
--lr LR
--wd WD
--warmup WARMUP
--max_tokens MAX_TOKENS
tokens per micro-batch (B*T)
--accum ACCUM
--max_ctx MAX_CTX
--brier_w BRIER_W
--label_smooth LABEL_SMOOTH
--eval_every EVAL_EVERY
--eval_limit EVAL_LIMIT
--train_cap TRAIN_CAP
cap total train examples (debug)
--seed SEED
--resample_every_epoch
--none_prob NONE_PROB
prob. of none-of-the-above augmentation per question
--schema_first_prob SCHEMA_FIRST_PROB
share of examples rendered questions-first (cacheable
schema prefix)
--max_options MAX_OPTIONS
options kept per question (10 = original protocol; 255
when the data set is already sub-sampled, e.g.
tasks_v6_delta)
This computer: torch.cuda.is_available() is False.
train.py moves the model to a CUDA graphics card with .cuda(), so it cannot train here.
scripts/train.sh as GitHub held them on 8 October 2026, and the options of the training script. Every fact in this step comes from its output. We did not run the training itself. It needs a data-centre graphics card, and this page was made on a laptop.เรารัน decider_training.py บนแล็ปท็อป ด้วย decider-ai 1.8.2 มันพิมพ์บรรทัดที่ยกมาข้างบนจากไฟล์ของไลบรารีเอง สิ่งที่ README พูดถึงโมเดลและการฝึก บรรทัดแรก ๆ ของ scripts/train.sh ตามที่อยู่บน GitHub เมื่อวันที่ 8 ตุลาคม 2026 และตัวเลือกของสคริปต์ฝึก ข้อเท็จจริงทุกข้อในขั้นนี้มาจากผลลัพธ์ของมัน เราไม่ได้รันตัวการฝึกเอง มันต้องใช้การ์ดจอระดับศูนย์ข้อมูล และหน้านี้ทำขึ้นบนแล็ปท็อป我们在笔记本上用 decider-ai 1.8.2 运行了 decider_training.py。它打印出上面引用的、来自这个库自身文件的那些代码行,README 里关于它的模型和关于训练的内容,2026 年 10 月 8 日 GitHub 上 scripts/train.sh 的开头几行,以及训练脚本的选项。这一步里的每一条事实都出自它的输出。我们没有运行训练本身:它需要数据中心级的显卡,而这一页是在一台笔记本上做出来的。
🧭 When to fine-tune, and when not to · เมื่อไรควรปรับจูน และเมื่อไรไม่ควร · 什么时候该微调,什么时候不该
🇬🇧 English
Fine-tuning is the right answer less often than people expect. Before you make four hundred examples, ask three questions.
- Would a prompt do? Step 3 is the test. For our tiny model the best prompt scored 1 of 50, so the answer was no. For a larger model it is often yes.
- Is the missing thing a fact? Prices, opening hours, this term's timetable: facts change, and a model that learned them in training goes on repeating the old ones. Keep facts in documents and let the model look them up as it answers. That is called retrieval, or RAG, and RAG with Python builds it for this same noodle shop. Our video about LoRA, which is in Thai, puts it this way: facts are for looking up, and habits are for training.
- Is the answer a choice among a few fixed options? Then a decision model such as decider may do it with no training at all.
Fine-tune when you want the model to behave in one way every time: to answer in a fixed form, to write in a certain voice, or to do one narrow job well on a small, cheap computer that keeps its data to itself. That is what the model on this page learned, and it is what fine-tuning is good at.
🇹🇭 ไทย
การปรับจูนเป็นคำตอบที่ถูกน้อยครั้งกว่าที่คนคิด ก่อนจะทำตัวอย่างสี่ร้อยชุด ให้ถามตัวเองสามข้อ
- ใช้พรอมต์ก็พอหรือเปล่า ขั้นที่ 3 คือการทดสอบข้อนี้ สำหรับโมเดลตัวจิ๋วของเรา พรอมต์ที่ดีที่สุดได้ 1 จาก 50 คำตอบจึงเป็นไม่พอ สำหรับโมเดลที่ใหญ่กว่า คำตอบมักเป็นพอ
- สิ่งที่ขาดอยู่คือข้อเท็จจริงหรือเปล่า ราคา เวลาเปิดปิด ตารางเรียนของเทอมนี้ ข้อเท็จจริงเปลี่ยนได้ และโมเดลที่เรียนมันมาจากการฝึกก็จะพูดซ้ำของเก่าอยู่อย่างนั้น ให้เก็บข้อเท็จจริงไว้ในเอกสาร แล้วให้โมเดลค้นดูตอนที่มันตอบ วิธีนี้เรียกว่า retrieval หรือ RAG และหน้า RAG ด้วย Python สร้างมันขึ้นมาให้ร้านก๋วยเตี๋ยวร้านเดียวกันนี้ วิดีโอเรื่อง LoRA ของเราพูดไว้ว่า ข้อเท็จจริงใช้ RAG ส่วนสไตล์กับนิสัยใช้ fine-tuning
- คำตอบเป็นการเลือกจากตัวเลือกตายตัวไม่กี่ข้อหรือเปล่า ถ้าใช่ โมเดลตัดสินใจอย่าง decider อาจทำได้โดยไม่ต้องฝึกอะไรเลย
ปรับจูนเมื่อคุณต้องการให้โมเดลทำตัวแบบเดียวกันทุกครั้ง เช่น ตอบในรูปแบบตายตัว เขียนด้วยน้ำเสียงแบบหนึ่ง หรือทำงานแคบ ๆ อย่างเดียวให้ดี บนคอมพิวเตอร์เล็ก ๆ ราคาถูกที่เก็บข้อมูลไว้กับตัวเอง นั่นคือสิ่งที่โมเดลในหน้านี้ได้เรียนรู้ และเป็นสิ่งที่การปรับจูนทำได้ดี
🇨🇳 中文
微调是正确答案的时候,比人们以为的要少。在做四百个样例之前,先问三个问题。
- 用提示词行不行?第 3 步就是这个检验。对我们这个很小的模型,最好的提示词只在 50 条里对了 1 条,所以答案是不行。对大一些的模型,答案常常是行。
- 缺的是不是事实?价格、营业时间、这个学期的课表:事实会变,而一个在训练中学到它们的模型,会一直重复旧的那些。把事实放在文档里,让模型在回答时去查。这叫作检索,或者 RAG,《用 Python 做 RAG》那一页就为这同一家面馆把它做了出来。我们讲 LoRA 的视频(泰语)是这样说的:事实靠查,习惯靠练。
- 答案是不是在几个固定选项里选一个?那么像 decider 这样的决策模型,也许完全不用训练就能做。
当你希望模型每一次都以同一种方式行事时,就微调:用固定的格式回答,用某种语气写作,或者在一台把数据留在自己手里的、便宜的小电脑上,把一件很窄的事做好。这一页的模型学会的就是这个,这也正是微调擅长的事。
- In
train.py, changer=8tor=2. Train the LoRA way again and runscore.py lora. Then tryr=32. The slider in step 8 tells you how many numbers each size trains. Does the handwritten score follow? - Train on fewer examples: in
train.py, changeread_examples("train.jsonl")toread_examples("train.jsonl")[:100]. How many examples does this job need? - Read the misses in step 10, and then teach the model the words it did not know. In
make_data.py, add phrases such as"pack it up"toPLACE_WORDSand"a couple of"toCOUNT_WORDS. Run it, train, and score again. Remember what step 10 said: after this the handwritten test is no longer a fair exam, because you have read it. - Teach the model that some messages are not orders. Decide what its reply to them should be, add examples of them to the training file, and change
read_orderinuse.pyto understand the new reply.
- ใน
train.pyเปลี่ยนr=8เป็นr=2ฝึกแบบ LoRA อีกครั้ง แล้วรันscore.py loraจากนั้นลองr=32แถบเลื่อนในขั้นที่ 8 บอกคุณว่าแต่ละขนาดฝึกตัวเลขกี่ตัว คะแนนของชุดทดสอบที่คนเขียนเองเปลี่ยนตามไปด้วยไหม - ฝึกด้วยตัวอย่างที่น้อยลง: ใน
train.pyเปลี่ยนread_examples("train.jsonl")เป็นread_examples("train.jsonl")[:100]งานนี้ต้องใช้ตัวอย่างกี่ชุด - อ่านข้อที่ผิดในขั้นที่ 10 แล้วสอนคำที่โมเดลไม่รู้จักให้มัน ใน
make_data.pyเพิ่มวลีอย่าง"pack it up"ลงในPLACE_WORDSและ"a couple of"ลงในCOUNT_WORDSรันมัน ฝึก แล้วให้คะแนนอีกครั้ง อย่าลืมสิ่งที่ขั้นที่ 10 บอกไว้: หลังจากนี้ ชุดทดสอบที่คนเขียนเองไม่ใช่ข้อสอบที่ยุติธรรมอีกต่อไป เพราะคุณอ่านมันแล้ว - สอนโมเดลว่าบางข้อความไม่ใช่ออร์เดอร์ ตัดสินใจว่าคำตอบของมันต่อข้อความแบบนั้นควรเป็นอย่างไร เพิ่มตัวอย่างของข้อความแบบนั้นลงในไฟล์ฝึก แล้วแก้
read_orderในuse.pyให้เข้าใจคำตอบแบบใหม่
- 在
train.py里,把r=8改成r=2。再用 LoRA 的方式训练一次,然后运行score.py lora。接着试试r=32。第 8 步的滑块会告诉你每种大小训练多少个数字。手写测试的分数跟着变了吗? - 用更少的样例训练:在
train.py里,把read_examples("train.jsonl")改成read_examples("train.jsonl")[:100]。这个任务需要多少个样例? - 读一读第 10 步里答错的那些,然后把模型不认识的词教给它。在
make_data.py里,往PLACE_WORDS里加上"pack it up"这样的短语,往COUNT_WORDS里加上"a couple of"。运行它,训练,再打一次分。记住第 10 步说过的话:在这之后,手写的测试就不再是一场公平的考试了,因为你已经读过它。 - 教会模型:有些消息不是订单。先决定它对这类消息应该怎样回复,把这类消息的样例加进训练文件,再修改
use.py里的read_order,让它能理解这种新的回复。
🇬🇧 English
Next: How decider works takes a fine-tuned model apart, and Python to EXE packs one into an app that opens with a double-click. To serve a model to a whole classroom, see vLLM instead of Ollama.
The model's own page is huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct. The documentation of the two libraries is at huggingface.co/docs/transformers and huggingface.co/docs/peft. LoRA comes from a paper of 2021, LoRA: Low-Rank Adaptation of Large Language Models, by Edward Hu and seven others.
🇹🇭 ไทย
ต่อไป: decider ทำงานอย่างไร แกะโมเดลที่ผ่านการปรับจูนให้ดูข้างใน และ Python to EXE แพ็กโมเดลแบบนั้นลงในแอปที่เปิดได้ด้วยการดับเบิลคลิก ถ้าอยากเสิร์ฟโมเดลให้ทั้งห้องเรียน ดูที่ vLLM แทน Ollama
หน้าของโมเดลเองอยู่ที่ huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct เอกสารของไลบรารีสองตัวอยู่ที่ huggingface.co/docs/transformers และ huggingface.co/docs/peft ส่วน LoRA มาจากบทความวิจัยปี 2021 ชื่อ LoRA: Low-Rank Adaptation of Large Language Models โดย Edward Hu และผู้เขียนอีกเจ็ดคน
🇨🇳 中文
接下来:《decider 是怎么工作的》把一个微调出来的模型拆开来讲,《Python to EXE》把这样的模型打包进一个双击就能打开的应用。想给整个教室提供模型服务,可以看《用 vLLM 代替 Ollama》。
模型自己的页面在 huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct。两个库的文档在 huggingface.co/docs/transformers 和 huggingface.co/docs/peft。LoRA 出自 2021 年的一篇论文:LoRA: Low-Rank Adaptation of Large Language Models,作者是 Edward Hu 和另外七人。