小白自己用PyTorch馴小語(yǔ)言模型 原創(chuàng)
訓(xùn)練語(yǔ)言模型就像教一個(gè)懵懂的小家伙學(xué)說(shuō)話(huà)——先給他喂足夠的書(shū),再教他理解詞語(yǔ)的關(guān)聯(lián),最后讓他學(xué)會(huì)順著話(huà)頭往下接。這個(gè)過(guò)程既有代碼的嚴(yán)謹(jǐn),更藏著數(shù)據(jù)與邏輯碰撞的靈性。下面咱們一步步拆解,每步都帶技術(shù)細(xì)節(jié),保證真實(shí)可落地。
一、準(zhǔn)備階段:給模型搭好"學(xué)習(xí)環(huán)境"
在開(kāi)始前,得先把工具備齊。這就像給學(xué)說(shuō)話(huà)的孩子準(zhǔn)備好紙筆和繪本,缺一不可。
1. 硬件與庫(kù)的基礎(chǔ)配置
- 硬件選擇:CPU不是不能練,但就像用自行車(chē)追高鐵——入門(mén)級(jí)模型(比如小LSTM)還能湊活,想玩Transformer就得有GPU。NVIDIA顯卡優(yōu)先,顯存至少4GB(推薦8GB以上),畢竟模型參數(shù)和數(shù)據(jù)都要占地方。PyTorch會(huì)自動(dòng)檢測(cè)設(shè)備,一行代碼就能搞定分配:
import torch device = 'cuda' if torch.cuda.is_available() else 'cpu' print(f"用的設(shè)備:{device}") # 能出cuda就偷著樂(lè)吧 - 必備庫(kù)安裝:直接用pip一鍵配齊,版本兼容問(wèn)題不大,PyTorch會(huì)自動(dòng)適配:
其中pip install torch torchvision torchaudio datasets tokenizers tqdmdatasets用來(lái)下公開(kāi)數(shù)據(jù),tokenizers處理文本,tqdm看訓(xùn)練進(jìn)度,都是干活的好手。
2. 數(shù)據(jù):模型的"精神食糧"
模型學(xué)什么全看你喂什么——喂唐詩(shī)能寫(xiě)絕句,喂代碼能調(diào)bug,喂菜譜能當(dāng)廚子。數(shù)據(jù)來(lái)源有兩個(gè)方向:
公開(kāi)數(shù)據(jù)集(新手首選)
- 英文選這個(gè):Hugging Face的
wikitext,全是維基百科的正經(jīng)內(nèi)容,分不同難度版本,比如wikitext-2適合練手,數(shù)據(jù)量不大但質(zhì)量高。加載代碼特簡(jiǎn)單:from datasets import load_dataset dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train") - 中文選這個(gè):
Chinese-CLUE/cluecorpussmall,包含新聞、小說(shuō)等多類(lèi)型文本,10G左右的數(shù)據(jù)量足夠喂大一個(gè)基礎(chǔ)模型,同樣在Hugging Face上能直接下。
自定義數(shù)據(jù)集(玩出個(gè)性)
要是想讓模型學(xué)你的文風(fēng),就把自己的文章、日記攢成txt文件,每行一段就行。注意至少要1萬(wàn)字以上——數(shù)據(jù)太少模型會(huì)"營(yíng)養(yǎng)不良",學(xué)出來(lái)凈說(shuō)胡話(huà)。
二、數(shù)據(jù)預(yù)處理:把文字變成模型能懂的"密碼"
人類(lèi)看文字懂意思,模型只認(rèn)數(shù)字。這一步就是給文字編密碼,核心是分詞器(Tokenizer)——相當(dāng)于模型的"字典"。
1. 訓(xùn)練專(zhuān)屬分詞器(關(guān)鍵步驟)
別用通用分詞器湊活,自定義的才合身。這里用GPT、T5都在用的BPE(字節(jié)對(duì)編碼)技術(shù),能處理生僻詞,比如"躺平"不會(huì)被拆成"躺"和"平"單獨(dú)理解。
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
from pathlib import Path
# 1. 初始化分詞器
tokenizer = Tokenizer(models.BPE(unk_token="<UNK>")) # 未知詞用<UNK>表示
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace() # 先按空格初步分割
# 2. 定義訓(xùn)練參數(shù)
trainer = trainers.BpeTrainer(
min_frequency=2, # 至少出現(xiàn)2次的詞才進(jìn)字典,過(guò)濾雜音
special_tokens=["<PAD>", "<UNK>", "<SOS>", "<EOS>"] # 特殊標(biāo)記:填充、未知、句首、句尾
)
# 3. 喂數(shù)據(jù)訓(xùn)練(把你的txt文件路徑放進(jìn)去)
file_paths = [str(file) for file in Path("./你的數(shù)據(jù)文件夾/").glob("*.txt")]
tokenizer.train(files=file_paths, trainer=trainer)
# 4. 保存?zhèn)溆茫麓沃苯佑貌挥迷儆?xùn)
tokenizer.save("./my_tokenizer.json")
訓(xùn)練完的分詞器會(huì)生成一個(gè)"字典",里面每個(gè)詞/子詞都對(duì)應(yīng)唯一數(shù)字,比如"你好"可能是1023,"世界"是2045。
2. 把文本切成"學(xué)習(xí)片段"
模型一次學(xué)不了一整篇小說(shuō),得切成短片段。比如每次學(xué)20個(gè)詞,輸入"今天天氣真好,適合出去",讓它預(yù)測(cè)下一個(gè)詞"玩"。
def process_text(text, tokenizer, seq_length=50):
# 1. 分詞轉(zhuǎn)數(shù)字
encoded = tokenizer.encode(text).ids
# 2. 切成輸入-目標(biāo)對(duì)
samples = []
for i in range(len(encoded) - seq_length):
input_seq = encoded[i:i+seq_length] # 輸入:前50個(gè)詞
target_seq = encoded[i+1:i+seq_length+1] # 目標(biāo):后50個(gè)詞(每個(gè)詞是輸入的下一個(gè))
samples.append((input_seq, target_seq))
return samples
# 用加載的數(shù)據(jù)集處理(以wikitext為例)
text = "\n".join([item["text"] for item in dataset if item["text"].strip()])
samples = process_text(text, tokenizer, seq_length=50)
# 3. 做成PyTorch能認(rèn)的數(shù)據(jù)集
from torch.utils.data import Dataset, DataLoader
class TextDataset(Dataset):
def __init__(self, samples):
self.samples = samples
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
input_seq, target_seq = self.samples[idx]
return torch.tensor(input_seq), torch.tensor(target_seq)
# 批量加載,batch_size根據(jù)顯存調(diào),4GB顯存選8,8GB選16
dataloader = DataLoader(TextDataset(samples), batch_size=16, shuffle=True)
三、搭建模型:給"小精靈"造個(gè)"大腦"
模型架構(gòu)就像大腦的神經(jīng)網(wǎng)絡(luò),新手推薦從簡(jiǎn)單的LSTM入手,上手快;想玩高級(jí)的就上Transformer——這可是ChatGPT的核心骨架,論文《Attention is all you need》里的經(jīng)典設(shè)計(jì)。
1. 入門(mén)版:LSTM語(yǔ)言模型
LSTM擅長(zhǎng)處理序列數(shù)據(jù),就像給模型裝了"短期記憶",能記住前面說(shuō)過(guò)的詞。
import torch.nn as nn
class LSTMLanguageModel(nn.Module):
def __init__(self, vocab_size, embedding_dim=128, hidden_dim=256, num_layers=2, dropout=0.2):
super().__init__()
self.vocab_size = vocab_size # 字典大小
self.embedding = nn.Embedding(vocab_size, embedding_dim) # 詞嵌入:把數(shù)字變成向量
self.lstm = nn.LSTM(
embedding_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout if num_layers>1 else 0 # 多層層才加dropout防過(guò)擬合
)
self.fc = nn.Linear(hidden_dim, vocab_size) # 輸出層:把LSTM結(jié)果轉(zhuǎn)成詞概率
self.dropout = nn.Dropout(dropout)
def forward(self, x, hidden=None):
# x形狀:(batch_size, seq_length)
embedded = self.dropout(self.embedding(x)) # 轉(zhuǎn)成向量:(batch, seq, embedding_dim)
lstm_out, hidden = self.lstm(embedded, hidden) # LSTM運(yùn)算
output = self.fc(self.dropout(lstm_out)) # 輸出:(batch, seq, vocab_size)
return output, hidden
# 初始化模型,vocab_size從分詞器拿
vocab_size = tokenizer.get_vocab_size()
model = LSTMLanguageModel(vocab_size=vocab_size).to(device) # 移到GPU
# 看看參數(shù)量,心里有譜:小模型幾十萬(wàn),大模型幾十億
print(f"模型參數(shù)量:{sum(p.numel() for p in model.parameters()):,}")
2. 進(jìn)階版:Transformer解碼器(生成專(zhuān)用)
要是想讓模型會(huì)寫(xiě)長(zhǎng)文本,就得用Transformer解碼器——帶掩碼的自注意力機(jī)制能保證預(yù)測(cè)時(shí)不"偷看"后面的詞,這是生成式模型的關(guān)鍵。
class TransformerDecoderModel(nn.Module):
def __init__(self, vocab_size, d_model=128, nhead=4, num_layers=2, dropout=0.2):
super().__init__()
self.d_model = d_model
self.embedding = nn.Embedding(vocab_size, d_model)
self.pos_encoder = nn.Embedding(512, d_model) # 位置編碼:告訴模型詞的順序
self.transformer_decoder = nn.TransformerDecoder(
nn.TransformerDecoderLayer(d_model=d_model, nhead=nhead, dropout=dropout, batch_first=True),
num_layers=num_layers
)
self.fc = nn.Linear(d_model, vocab_size)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
seq_len = x.size(1)
# 位置編碼:每個(gè)位置對(duì)應(yīng)一個(gè)向量
pos = torch.arange(0, seq_len, device=device).unsqueeze(0) # (1, seq_len)
embedded = self.dropout(self.embedding(x) + self.pos_encoder(pos)) # 詞嵌入+位置編碼
# 生成掩碼,防止偷看后面的詞
mask = nn.Transformer.generate_square_subsequent_mask(seq_len).to(device)
out = self.transformer_decoder(embedded, memory=None, tgt_mask=mask)
return self.fc(out)
# 用法和LSTM一樣,初始化后移到GPU
model = TransformerDecoderModel(vocab_size=vocab_size).to(device)
四、訓(xùn)練模型:讓"小精靈"開(kāi)始學(xué)習(xí)
這一步是最磨人的,就像陪孩子寫(xiě)作業(yè)——得盯著進(jìn)度,還得及時(shí)糾錯(cuò)。核心是損失函數(shù)(判斷說(shuō)得對(duì)不對(duì))和優(yōu)化器(改正好錯(cuò)誤)。
1. 配置訓(xùn)練工具
# 損失函數(shù):交叉熵,專(zhuān)門(mén)算分類(lèi)問(wèn)題的誤差,忽略填充符<PAD>
criterion = nn.CrossEntropyLoss(ignore_index=tokenizer.token_to_id("<PAD>"))
# 優(yōu)化器:Adam,比SGD聰明,學(xué)習(xí)率0.001是黃金起點(diǎn)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 訓(xùn)練輪次:新手先跑10輪看看,數(shù)據(jù)多就加
num_epochs = 10
2. 核心訓(xùn)練循環(huán)
from tqdm import tqdm # 進(jìn)度條,看著心里不慌
model.train() # 切換到訓(xùn)練模式
for epoch in range(num_epochs):
total_loss = 0.0
# 用tqdm顯示進(jìn)度,desc是本輪的標(biāo)簽
for batch in tqdm(dataloader, desc=f"第{epoch+1}輪訓(xùn)練"):
input_seq, target_seq = batch[0].to(device), batch[1].to(device)
# 1. 前向傳播:讓模型預(yù)測(cè)
if isinstance(model, LSTMLanguageModel):
output, _ = model(input_seq) # LSTM要返回hidden,這里用不上
else:
output = model(input_seq) # Transformer直接出結(jié)果
# 2. 計(jì)算損失:output形狀是(batch, seq, vocab),要轉(zhuǎn)成(batch*seq, vocab)才符合交叉熵要求
loss = criterion(output.view(-1, vocab_size), target_seq.view(-1))
# 3. 反向傳播:算梯度(PyTorch自動(dòng)求導(dǎo),不用自己推公式)
optimizer.zero_grad() # 先清掉上一輪的梯度,不然會(huì)累加
loss.backward() # 從損失往回算每個(gè)參數(shù)的梯度
# 4. 更新參數(shù):讓模型改正好錯(cuò)誤
optimizer.step()
total_loss += loss.item()
# 每輪結(jié)束打印損失,損失越來(lái)越小才對(duì)
avg_loss = total_loss / len(dataloader)
print(f"第{epoch+1}輪結(jié)束,平均損失:{avg_loss:.4f}")
# 保存模型,防止斷電前功盡棄
torch.save(model.state_dict(), f"./model_epoch_{epoch+1}.pth")
關(guān)鍵判斷:如果損失降到1.0以下,說(shuō)明模型已經(jīng)學(xué)懂點(diǎn)東西了;要是損失不降反升,要么是學(xué)習(xí)率太高,要么是數(shù)據(jù)太少,得調(diào)參。
五、測(cè)試模型:聽(tīng)聽(tīng)"小精靈"怎么說(shuō)話(huà)
訓(xùn)練完就得驗(yàn)收成果,讓模型從"今天天氣真好"往下接話(huà),看看是不是人話(huà)。
def generate_text(model, tokenizer, start_text, max_len=100):
model.eval() # 切換到評(píng)估模式,不訓(xùn)練只預(yù)測(cè)
# 1. 把開(kāi)頭文本轉(zhuǎn)成數(shù)字
input_ids = tokenizer.encode(start_text).ids
input_tensor = torch.tensor(input_ids).unsqueeze(0).to(device) # 加batch維度
with torch.no_grad(): # 預(yù)測(cè)時(shí)不用算梯度,省顯存
for _ in range(max_len):
if isinstance(model, LSTMLanguageModel):
output, hidden = model(input_tensor)
else:
output = model(input_tensor)
hidden = None # Transformer不用hidden
# 取最后一個(gè)詞的預(yù)測(cè)概率,選概率最大的那個(gè)(貪心搜索)
next_token_logits = output[:, -1, :]
next_token_id = torch.argmax(next_token_logits, dim=-1, keepdim=True)
# 把新預(yù)測(cè)的詞加進(jìn)去,繼續(xù)預(yù)測(cè)下一個(gè)
input_tensor = torch.cat([input_tensor, next_token_id], dim=1)
# 遇到句尾符就停
if next_token_id.item() == tokenizer.token_to_id("<EOS>"):
break
# 把數(shù)字轉(zhuǎn)成文字
generated_ids = input_tensor.squeeze(0).cpu().numpy().tolist()
return tokenizer.decode(generated_ids)
# 測(cè)試一下,比如讓模型接"人工智能的未來(lái)"
result = generate_text(model, tokenizer, start_text="人工智能的未來(lái)", max_len=100)
print("生成結(jié)果:", result)
六、避坑指南:少走99%的彎路
-
顯存不夠怎么辦?
把batch_size調(diào)小(比如從16改成8),seq_length縮短(從50改成30),再不行就用torch.cuda.empty_cache()清顯存。 -
模型學(xué)完凈說(shuō)胡話(huà)?
大概率是數(shù)據(jù)太少或質(zhì)量差——至少加10萬(wàn)字文本,別喂亂七八糟的拼湊內(nèi)容。另外可以加dropout(調(diào)到0.3)防過(guò)擬合。 -
訓(xùn)練速度太慢?
先確認(rèn)用了GPU(打印device看是不是cuda),再安裝apex庫(kù)開(kāi)啟混合精度訓(xùn)練,能快一倍還不丟精度。 -
想省時(shí)間?微調(diào)預(yù)訓(xùn)練模型!
從零訓(xùn)練太費(fèi)時(shí)間,新手可以直接用Hugging Face的預(yù)訓(xùn)練模型(比如distilgpt2)微調(diào)——把別人訓(xùn)好的大模型拿來(lái)改改,幾小時(shí)就能出效果。
最后:模型的"靈性"從哪來(lái)?
其實(shí)模型本身沒(méi)有靈魂,但它學(xué)的每一個(gè)詞、每一句話(huà)都帶著你喂給它的數(shù)據(jù)的溫度。喂詩(shī)三百首,它能吟出平仄;喂人間煙火,它能講出生活。訓(xùn)練模型的過(guò)程,本質(zhì)上是讓數(shù)據(jù)里的智慧通過(guò)代碼流動(dòng)起來(lái)——這大概就是技術(shù)最浪漫的地方。

















