Most people who say they're "learning AI" really only do one thing: get someone else's model running on their laptop.
This piece is not a "click Run All and you're done" walkthrough. It is a record of writing the training loop by hand, persisting the weights, and finally turning them into a small but real HTTP API.
Hands-on Colab
If you want to run the full version yourself, I also made an English Colab notebook. It writes
train.py, trains the model, savesmodel.pt, starts a FastAPI server, and calls/predictonce so you can see the whole loop actually work.
The Core Question: What Is the Model Actually Learning?
The starting question was simple: why does running a pile of math mean the model has "learned" anything?
To take that question seriously, I avoided complex networks and went back to basics — a tiny but complete project that can actually be called over HTTP. The principles I followed:
- No pre-made datasets
- No
model.fit() - Write the training loop myself
- Turn the model into an API myself
- Persist the weights, so the API does not silently rely on training-time variables
Step 1: Simulating Real-World Noise
A model does not start from rules — it starts from data. So I first defined a "real-world" relationship and added measurement noise on top of it:
y = 3x + 2 + noise
import torch
# 100 random observations
x = torch.randn(100, 1)
# True relationship plus noise
y = 3 * x + 2 + 0.1 * torch.randn(100, 1)
The point of this step: the goal of training is not to "find the truth," but to reverse-engineer the relationship hiding inside messy data.
Step 2: A Model Is Just a Function with Weights
Without nn.Linear, a model is just a linear function with two trainable parameters:
ŷ = wx + b
# Random initialization
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
def model(x):
return x * w + b
At this point w and b are random guesses. The model knows nothing about the world yet.
Step 3: The Loss Function Is the Model's "Values"
A model does not know what is "right" — it only knows what is "wrong." I defined that wrongness with Mean Squared Error (MSE):
def loss_fn(y_pred, y_true):
return ((y_pred - y_true) ** 2).mean()
That single line tells the model:
"The further your prediction, the higher the cost. Your entire job is to drive this number down."
Step 4: Training Is Controlled Wandering
This is the heart of the project. Training is not magic — it is five steps, repeated:
- Predict — feed in
x, get a prediction - Measure error — compare against the true
y - Compute the gradient — find the direction that lowers the error (backpropagation)
- Update parameters — take a small step in that direction
- Repeat
for epoch in range(100):
y_pred = model(x)
loss = loss_fn(y_pred, y)
# Backpropagation
loss.backward()
# Update outside the autograd graph
with torch.no_grad():
w -= 0.1 * w.grad
b -= 0.1 * b.grad
# Gradients accumulate by default — clear them
w.grad.zero_()
b.grad.zero_()
After enough epochs, you will see w ≈ 3 and b ≈ 2 — not because the model "understands" arithmetic, but because at those two numbers, the loss surface is at its lowest point.
Step 5: Save the Trained Weights
This is the step that most "from zero" tutorials skip. After training, the only thing that actually matters is two numbers, w and b. If they live only inside the training script, the API later will silently depend on training-time globals — and that dependency will break the moment the script is no longer running.
So before going anywhere near a server, persist them:
import torch
# Persist the trained parameters to disk
torch.save({"w": w.detach(), "b": b.detach()}, "model.pt")
detach() strips the autograd graph; saved tensors should not carry gradient state. From this point on, model.pt is the real deliverable, and everything downstream is allowed to assume it exists.
Step 6: Serve the Model Through FastAPI
With the weights on disk, the API has a clean contract: load once at startup, then do pure math per request.
import torch
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Load the trained weights once, at startup
state = torch.load("model.pt")
w = state["w"]
b = state["b"]
class PredictRequest(BaseModel):
x: float
@app.post("/predict")
def predict(req: PredictRequest):
# Inference: pure computation, no gradients
with torch.no_grad():
y = req.x * w.item() + b.item()
return {"result": y}
The endpoint is now self-contained — it does not depend on any variable left over from the training script. A client just sends:
POST /predict
{ "x": 1.5 }
// Response:
{ "result": 6.5 }
The caller does not need to know what PyTorch is, what loss function was used, or how many epochs were run.
This Is Not a Toy — It Is a Miniature Real System
The model in this project is laughably small, but its lifecycle is the same as anything shipped at GitHub, Netflix, or OpenAI:
- Data preparation (Step 1)
- Architecture choice (Step 2)
- Objective and optimisation (Steps 3 & 4)
- Persistence (Step 5)
- Serving (Step 6)
Three Things I Took Away
1. AI is not if/else.
It is a function shaped by data, not by hand-written rules.
2. The model is not the deliverable — the interface is.
A model that nothing can call is just a file on disk.
3. The underlying loop is universal.
Whether the model is ChatGPT or this y = wx + b, the core idea is the same: minimise an objective function under a stream of data.
Walking the whole loop end-to-end — generate, train, persist, serve — is what turns "I read about AI" into "I built one, and it runs."
很多人說自己在學 AI,但實際上做到的只有一件事:「讓別人的模型在自己的電腦跑起來。」
這篇文章不是那種點點 Run All 就結束的教學,而是一份完整的紀錄:自己寫訓練流程、把訓練後的權重存下來,最後用一支真的可以被呼叫的 FastAPI 服務把它變成 API。
想自己動手跑一次?
我另外做了一份純中文 Colab。它會建立
train.py、訓練模型、輸出model.pt、啟動 FastAPI,最後實際呼叫一次/predict,讓你從頭到尾跑完整個流程。
核心問題:模型到底在學什麼?
最一開始的疑問很單純:為什麼把一堆數學跑一跑,模型就會被認為「學會」了某件事?
為了認真回答這個問題,我刻意不去碰複雜的神經網路,而是回到最基本的樣貌——一個小、但完整、可以被外部呼叫的專案。我給自己訂了幾條原則:
- 不用現成資料集
- 不用
model.fit() - 自己寫訓練流程
- 自己把模型變成 API
- 把訓練好的權重存下來,避免 API 偷偷依賴訓練時的變數
Step 1:模擬真實世界的雜訊
模型不是從規則開始,而是從資料開始。所以我先定義一條「真實世界」的關係式,然後在它身上加上觀測雜訊:
y = 3x + 2 + noise
import torch
# 100 個隨機觀測點
x = torch.randn(100, 1)
# 真實規律加上雜訊
y = 3 * x + 2 + 0.1 * torch.randn(100, 1)
這一步的核心觀念是:訓練的目的不是找出「真理」,而是從一堆混亂的資料中,反推出背後的關係。
Step 2:模型其實是一個帶有權重的函數
在沒有 nn.Linear 的情況下,模型只是一個含有兩個可訓練參數的線性函數:
ŷ = wx + b
# 隨機初始化參數
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
def model(x):
return x * w + b
此時的 w 與 b 都是亂數,模型對這個世界一無所知。
Step 3:Loss 函數是模型的「價值觀」
模型不知道什麼是「對」,它只知道什麼是「錯」。我用**均方誤差(MSE)**來定義錯誤:
def loss_fn(y_pred, y_true):
return ((y_pred - y_true) ** 2).mean()
這一行其實是在跟模型說:
「你預測得越偏,代價就越高,你存在的意義就是把這個數字壓低。」
Step 4:訓練是「受控的亂走」
這是整個專案的核心。訓練不是魔法,只是把以下五步反覆做:
- 預測:丟入
x,得到預測值 - 算錯:與真實
y比對誤差 - 算梯度:找出讓誤差下降的方向(Backpropagation)
- 更新參數:往那個方向跨一小步
- 重來
for epoch in range(100):
y_pred = model(x)
loss = loss_fn(y_pred, y)
# 反向傳播
loss.backward()
# 更新權重,不記錄到計算圖裡
with torch.no_grad():
w -= 0.1 * w.grad
b -= 0.1 * b.grad
# 梯度預設會累加,要手動清掉
w.grad.zero_()
b.grad.zero_()
跑完之後你會看到 w ≈ 3 與 b ≈ 2——不是因為模型懂數學,而是因為在這兩個數字下,Loss 是整個曲面最低的位置。
Step 5:把訓練好的權重存下來
這是大多數「從零開始」教學會跳過的一步。訓練結束後真正重要的就是兩個數字:w 與 b。如果它們只活在訓練腳本裡,後面的 API 就會偷偷依賴訓練時的全域變數——一旦訓練腳本不再執行,那份依賴關係就會立刻斷掉。
所以在還沒碰到伺服器之前,先把它們寫到檔案裡:
import torch
# 將訓練好的參數寫到磁碟
torch.save({"w": w.detach(), "b": b.detach()}, "model.pt")
detach() 會把 autograd 計算圖剝掉——存檔的張量不應該還帶著梯度狀態。從這一刻起,model.pt 就是真正的交付物,後續的所有程式碼都可以放心假設它存在。
Step 6:用 FastAPI 把模型變成 API
權重存在磁碟上之後,API 的契約就很乾淨:啟動時載入一次,每個請求只做純計算。
import torch
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 啟動時載入訓練好的權重
state = torch.load("model.pt")
w = state["w"]
b = state["b"]
class PredictRequest(BaseModel):
x: float
@app.post("/predict")
def predict(req: PredictRequest):
# 推論:只做純計算,不算梯度
with torch.no_grad():
y = req.x * w.item() + b.item()
return {"result": y}
這個服務是自包覆的,不依賴任何訓練腳本留下來的全域變數。呼叫方只要送:
POST /predict
{ "x": 1.5 }
// Response:
{ "result": 6.5 }
呼叫方完全不需要知道什麼是 PyTorch、用了什麼 Loss 函數、跑了幾個 epoch。
這不是玩具,是一個縮小版的真實系統
這個模型小到很可笑,但它的生命週期和 GitHub、Netflix、OpenAI 這些公司的模型完全一致:
- 資料準備(Step 1)
- 架構選擇(Step 2)
- 目標與最佳化(Step 3 & 4)
- 權重持久化(Step 5)
- 對外服務(Step 6)
我從這個流程學到的三件事
1. AI 不是 if/else。
它是被資料形塑出來的函數,而不是手寫規則。
2. 模型本身不是交付物,介面才是。
沒有任何東西能呼叫的模型,只是硬碟上的一個檔案。
3. 底層循環是共通的。
不論是 ChatGPT 還是這個 y = wx + b,核心都是同一件事:在一份持續流入的資料下,最小化某個目標函數。
把整個流程從頭走一次——生成、訓練、保存、服務化——就是把「我看過 AI」變成「我做過、它也真的跑得起來」的那一步。