LLM 原理:從數學直覺到 Python 實作:拆解大型語言模型如何「產生答案」

前置需求(與資料來源)
- 具備基礎 Python 知識
- 會用到 PyTorch 與 Hugging Face Transformers
- PyTorch 官方文件:https://pytorch.org/
- Transformers 官方文件:https://huggingface.co/docs/transformers/index
- 建議使用 Google Colab 直接跑範例(免安裝、可用雲端算力):
https://colab.research.google.com/ - 本文主軸參考:李宏毅老師《生成式人工智慧與機器學習導論 2025》影片(第 1 講)
- 模型與資源(Hugging Face Hub):https://huggingface.co/
- 若用 Meta Llama 3.2:模型(需申請授權)
https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct
- 若用 Meta Llama 3.2:模型(需申請授權)
1. Tokenizer:AI 的「翻譯蒟蒻」(Encoding & Decoding)
核心概念(通俗版)
可以把 Tokenizer 想成「把文字變成積木編號」的機器。
- 人類看的是「字」
- 模型吃的是「數字」(token IDs)
- Tokenizer 做的事就是:切分文字 → 查表 → 變成一串 ID
換個方式想:餐廳點餐
你走進一家超嚴格的餐廳,菜單上有十幾萬道菜。你不能直接喊「我要宮保雞丁」,廚師聽不懂;你得先查菜單,報出編號「#89757」。
重點
- Encode(編碼/點餐):把文字轉成 token IDs(例如
[20320, ...]) - Decode(解碼/上菜):把 token IDs 轉回字串(可讀的中文)
補充:常會看到「128K 詞表」這類說法;實務上
len(tokenizer)可能會比 128,000 多一些,因為還包含特殊 token(例如 BOS/EOS、控制符號等)。
(想看例子可參考社群討論:https://huggingface.co/meta-llama/Llama-3.2-1B/discussions/34)
Python 實作
from transformers import AutoTokenizer
# 指定模型 ID(Tokenizer 會跟著模型走,因為不同模型的切分規則可能不同)
model_id = "meta-llama/Llama-3.2-3B-Instruct"
# 載入 Tokenizer(等同拿到「菜單+編號對照表」)
tokenizer = AutoTokenizer.from_pretrained(model_id)
text = "你好,台灣"
# 方式 A:encode / decode(最直觀,適合觀察)
# add_special_tokens=False:不自動加上 BOS/EOS 等特殊 token,方便純觀察「文字被切成什麼」
input_ids = tokenizer.encode(text, add_special_tokens=False)
print("原始文字:", text)
print("轉譯 ID:", input_ids)
# decode:把 IDs 轉回文字
# skip_special_tokens=True:若有特殊 token,會在 decode 時略過,避免輸出怪符號
decoded_text = tokenizer.decode(input_ids, skip_special_tokens=True)
print("還原文字:", decoded_text)
# 方式 B:更常見的寫法(回傳 tensor + attention_mask)
# attention_mask:告訴模型「哪些位置是有效內容(1)/ 哪些是 padding(0)」
batch = tokenizer(text, return_tensors="pt", add_special_tokens=False)
print(batch.keys()) # dict_keys(['input_ids', 'attention_mask'])
print("input_ids shape:", batch["input_ids"].shape)
print("attention_mask shape:", batch["attention_mask"].shape)
2. Model 本體:巨大的「評分機器」(Logits)
核心概念
如果 Tokenizer 是「把文字換成編號」,那模型就是「對下一個字做全班排名」的機器。
模型不會直接輸出答案,它會先對「字典裡每個候選 token」打分數,這份分數表就叫 logits。
換個方式想:選秀大會 輸入「台灣最高的山是」,模型不會直接說答案;它會對所有候選 token 打分數:
- 候選「玉」:980 分(logit 高)
- 候選「阿」:300 分(logit 中)
- 候選「喜」:-50 分(logit 低)
Python 實作
import torch
from transformers import AutoModelForCausalLM
model_id = "meta-llama/Llama-3.2-3B-Instruct"
# 載入模型(這就是那個「打分數的評審團」)
model = AutoModelForCausalLM.from_pretrained(model_id)
model.eval() # 推論模式:關閉 dropout 等訓練時才需要的行為
text = "台灣最高的山是"
# tokenizer(...) 會回傳 input_ids + attention_mask
inputs = tokenizer(text, return_tensors="pt")
# 不做梯度計算,省記憶體也更快
with torch.no_grad():
outputs = model(**inputs)
# outputs.logits shape = [batch_size, seq_len, vocab_size]
# - batch_size:一次餵幾筆資料(這裡 1)
# - seq_len:輸入字串被切成多少個 token
# - vocab_size:詞表大小(候選 token 的總數)
logits = outputs.logits
print("logits shape:", logits.shape)
# 我們想看「下一個 token」的分數,所以取最後一個位置(-1)
# next_token_logits shape = [vocab_size]
next_token_logits = logits[0, -1, :]
print("vocab_size:", next_token_logits.shape[0])
# argmax:分數最高的候選 token_id(相當於「第一名」)
top1_id = torch.argmax(next_token_logits).item()
print("最高分 token_id:", top1_id)
print("最高分 token(解碼後):", tokenizer.decode([top1_id], skip_special_tokens=True))
3. Softmax:把分數變成機率(Probability)
核心概念
logits 是「分數」,但分數很難直覺比較差距;Softmax 會把分數轉成「百分比」,讓你一眼看懂:
- 每個候選 token 都會得到 0~1 的機率
- 所有機率加總 = 1(也就是 100%)
(Softmax 官方說明:https://docs.pytorch.org/docs/stable/generated/torch.nn.Softmax.html)
換個方式想:得票率
- 980 分 → 可能變成 0.95
- 300 分 → 0.04
- -50 分 → 0.000001
Python 實作
import torch
# 把 logits 轉成機率分佈
probs = torch.softmax(next_token_logits, dim=-1)
# 取出機率最高的前 5 名
top5_probs, top5_ids = torch.topk(probs, 5)
print("--- 下一 token 機率 Top 5 ---")
for p, idx in zip(top5_probs, top5_ids):
token_str = tokenizer.decode([idx.item()], skip_special_tokens=True)
print(f"token: {token_str!r} | p={p.item():.4f}")
4. Sampling:更有創意的「擲骰子」(Temperature / Top-k / Top-p)
核心概念
有了機率後,模型「不一定」永遠選第一名。
可以把它想成:轉盤的每個格子大小 = token 機率,然後由使用者決定要「直接選最大格」或「轉一下抽到誰算誰」。
-
Greedy Search:永遠選機率最高(穩、可控,但容易無聊或重複)
-
Sampling:照機率抽樣(更像人、更有變化,但可能更發散)
-
Temperature:控制「轉盤差距」
- 低溫(<1):差距變大,模型更保守
- 高溫(>1):差距變小,模型更隨機
-
Top-k / Top-p:先把候選名單縮小,再抽(避免抽到太離譜的)
小提醒 :
temperature/top_k/top_p這些參數通常要搭配do_sample=True才會生效。 參考官方生成參數說明:https://huggingface.co/docs/transformers/v4.25.1/en/main_classes/text_generation 以及策略概覽:https://huggingface.co/docs/transformers/generation_strategies
Python 實作(手動溫度縮放 + top-k 抽樣)
import torch
temperature = 0.7 # 越小越保守,越大越隨機
# 1) 溫度縮放:把 logits 除以 temperature,再做 softmax
# - temperature < 1:讓高分更突出(更「冷靜」)
# - temperature > 1:讓分佈更平均(更「放飛」)
scaled_logits = next_token_logits / temperature
scaled_probs = torch.softmax(scaled_logits, dim=-1)
# 2) 只保留 top-k 的候選(避免抽到太怪的 token)
top_k = 50
topk_probs, topk_ids = torch.topk(scaled_probs, top_k)
# 3) 重新正規化(因為只剩 top-k,總和不一定是 1)
topk_probs = topk_probs / topk_probs.sum()
# 4) multinomial:按照機率抽樣(真正的「擲骰子」)
# 官方文件:https://docs.pytorch.org/docs/stable/generated/torch.multinomial.html
sampled_index = torch.multinomial(topk_probs, num_samples=1).item()
sampled_token_id = topk_ids[sampled_index].item()
print("抽到的 token:", tokenizer.decode([sampled_token_id], skip_special_tokens=True))
Python 實作(實務:直接用 generate)
generation_kwargs = dict(
max_new_tokens=50, # 最多生成 50 個新 token
do_sample=True, # 開啟 sampling(否則 temperature/top_k/top_p 多半不生效)
temperature=0.7, # 溫度:控制隨機程度
top_k=50, # 只在前 50 名候選中抽
# top_p=0.9, # 也可以改用 top-p(nucleus sampling)
)
output_ids = model.generate(**inputs, **generation_kwargs)
print(tokenizer.decode(output_ids[0], skip_special_tokens=True))
5. 多輪對話的真相:上下文拼接+Chat Template
核心概念
聊天模型看起來像「有記憶」,但更精確地說是:
- 模型本身沒有跨回合的永久記憶
- 它能「看起來記得」,是因為把對話歷史一起塞回 prompt
- 模型只是讀到「上一輪使用者說了什麼」,才接著往下寫
而不同聊天模型「吃的格式」不一樣(控制符號、角色標記都不同),所以 Transformers 提供 Chat Template 來把 messages 自動格式化:
換個方式想:劇本抄寫員 使用者問第二句「那第二高呢?」如果只丟這句,模型根本不知道使用者在比什麼; 使用者要把前情「最高的是玉山」也一起給它,它才接得上。
Python 實作(用 messages + chat template)
import torch
messages = [
{"role": "user", "content": "台灣最高的山是哪一座?"},
{"role": "assistant", "content": "玉山。"},
{"role": "user", "content": "那第二高呢?"},
]
# apply_chat_template:把 messages 轉成模型所需的格式(包含角色、控制符號等)
# add_generation_prompt=True:在尾端加上「輪到 assistant 回答」的提示,讓模型接著生成
formatted = tokenizer.apply_chat_template(
messages,
tokenize=True, # 直接回傳可餵給模型的 token
return_dict=True, # 回傳 dict(含 input_ids / attention_mask)
add_generation_prompt=True
)
with torch.no_grad():
out = model.generate(**formatted, max_new_tokens=50)
print(tokenizer.decode(out[0], skip_special_tokens=True))
6. Pipeline:懶人專用的「一鍵完成」
核心概念
Pipeline 就像「料理包」:
- 使用者不用自己買麵粉、桿麵、燉湯(Tokenizer / Template / generate / decode)
- 使用者只要丟文字(或 messages),Pipeline 幫使用者串好整套流程
Text Generation Pipeline 官方文件:https://huggingface.co/docs/transformers/main_classes/pipelines 其中也明確說明:可以直接傳入「list of dicts with role/content」的 chat,Pipeline 會自動套用模型的 chat template。
Python 實作
import torch
from transformers import pipeline
model_id = "meta-llama/Llama-3.2-3B-Instruct"
# 建立 text-generation pipeline
# device_map="auto":若環境有 GPU,通常會自動用上(依實際裝置與權限而定)
# torch_dtype:減少記憶體占用,常用 float16/bfloat16(依硬體支援)
pipe = pipeline(
"text-generation",
model=model_id,
torch_dtype=torch.bfloat16,
device_map="auto",
)
messages = [
{"role": "system", "content": "你是一個講解清楚、重視事實的助教。"},
{"role": "user", "content": "台灣最高的山是哪一座?"},
]
# Pipeline 會:套 chat template → tokenize → generate → decode
outputs = pipe(messages, max_new_tokens=80)
# generated_text 通常是一段含對話結構的結果;不同版本回傳格式可能略有差異
print(outputs[0]["generated_text"][-1])
結論:打破 AI 的神話
透過以上拆解,可以把「看起來很神」的部分落回可驗證的機制:
- 它不識字:它處理的是 Tokenizer 映射後的 token IDs 與向量。
- 它沒有神秘直覺:核心是把輸入變成 logits,再把 logits 轉成機率分佈。
- 它的「創意」可被調參:temperature、top-k / top-p 等策略會直接改變輸出分佈與風格。
- 它的「記憶」來自上下文:多輪對話多半是把歷史訊息用 chat template 格式化後放回輸入中(官方說明:https://huggingface.co/docs/transformers/en/chat_templating)。
附錄:常用工具懶人包(含資料來源)
Google Colab
- 比喻:Google 借你的雲端電腦
- 重點:免安裝、可直接跑 Notebook;常用於模型推論與教學示範
- 連結:https://colab.research.google.com/
Hugging Face
-
比喻:AI 界的 GitHub
-
重點:模型、資料集、Tokenizer、Pipeline 與社群資源的集中地
-
連結:
- Hub:https://huggingface.co/
- Transformers Docs:https://huggingface.co/docs/transformers/index
- Chat Template Docs:https://huggingface.co/docs/transformers/en/chat_templating
- Pipelines Docs:https://huggingface.co/docs/transformers/main_classes/pipelines