Training a Model from ZeroThen Turning It into an API

A hands-on record of writing a training loop by hand, persisting the weights, and serving them through a small FastAPI endpoint, with an Colab notebook for running the version yourself.

7 min read
··· views

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, saves model.pt, starts a FastAPI server, and calls /predict once so you can see the whole loop actually work.

Open the English Colab notebook

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:

  1. No pre-made datasets
  2. No model.fit()
  3. Write the training loop myself
  4. Turn the model into an API myself
  5. 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:

  1. Predict — feed in x, get a prediction
  2. Measure error — compare against the true y
  3. Compute the gradient — find the direction that lowers the error (backpropagation)
  4. Update parameters — take a small step in that direction
  5. 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,讓你從頭到尾跑完整個流程。

打開中文 Colab 筆記本

核心問題:模型到底在學什麼?

最一開始的疑問很單純:為什麼把一堆數學跑一跑,模型就會被認為「學會」了某件事?

為了認真回答這個問題,我刻意不去碰複雜的神經網路,而是回到最基本的樣貌——一個小、但完整、可以被外部呼叫的專案。我給自己訂了幾條原則:

  1. 不用現成資料集
  2. 不用 model.fit()
  3. 自己寫訓練流程
  4. 自己把模型變成 API
  5. 把訓練好的權重存下來,避免 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

此時的 wb 都是亂數,模型對這個世界一無所知。

Step 3:Loss 函數是模型的「價值觀」

模型不知道什麼是「對」,它只知道什麼是「錯」。我用**均方誤差(MSE)**來定義錯誤:

def loss_fn(y_pred, y_true):
    return ((y_pred - y_true) ** 2).mean()

這一行其實是在跟模型說:

「你預測得越偏,代價就越高,你存在的意義就是把這個數字壓低。」

Step 4:訓練是「受控的亂走」

這是整個專案的核心。訓練不是魔法,只是把以下五步反覆做:

  1. 預測:丟入 x,得到預測值
  2. 算錯:與真實 y 比對誤差
  3. 算梯度:找出讓誤差下降的方向(Backpropagation)
  4. 更新參數:往那個方向跨一小步
  5. 重來
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 ≈ 3b ≈ 2——不是因為模型懂數學,而是因為在這兩個數字下,Loss 是整個曲面最低的位置

Step 5:把訓練好的權重存下來

這是大多數「從零開始」教學會跳過的一步。訓練結束後真正重要的就是兩個數字:wb。如果它們只活在訓練腳本裡,後面的 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」變成「我做過、它也真的跑得起來」的那一步。

Comments