
什么是GPT-4?完整指南
首先我們先來看config文件,它的作用是用來集中管理各種訓練配置。簡單來說,這個類就像是一個工具箱,里面放著模型訓練過程中需要的各種“工具”和“參數”。這些參數直接影響模型的性能和訓練流程。
import torch
class CFG:
model_name = 'bert_models/ModernBert-base' # 模型路徑
tokenizer_path = 'bert_models/ModernBert-base'
max_length = 1024 # 輸入文本的最大長度
batch_size = 8 # 訓練時的批量大小
learning_rate = 2e-5 # 學習率
step_size = 7 # 學習率調度器的步進大小
gamma = 0.1 # 學習率調度器的衰減因子
num_classes = 2 # 分類的類別數
pooling = 'attention' # 池化方法
epochs = 1 # 訓練的輪數
grad_clip=False
device = 'cuda' if torch.cuda.is_available() else 'cpu'
train_path = 'dataset/train.csv'
valid_path = 'dataset/evaluation.csv'
target_cols = ['label']
test_path = 'dataset/test.csv'
best_model_path = 'output/best_model.pth'
比如,model_name
和 tokenizer_path
指向模型和分詞器的位置,這里使用了前幾天剛發布的 ModernBert-base
。模型和分詞器的路徑需要一致,因為分詞器的輸出要和模型的輸入匹配。
max_length
是輸入文本的最大長度,比如1024。如果文本超過這個長度,模型就會截斷多余的部分。這個值設置得太短可能會丟失重要信息,太長又會增加計算量。
batch_size
是每次訓練時處理的樣本數量,值為 8,意味著一次訓練會同時處理 8 條數據。這個值和顯存大小密切相關,顯存大的設備可以設置更大的批量。
learning_rate
是學習率,決定模型在訓練中參數更新的步伐。這里的值是 2e-5
,是一個比較小的值,適合微調預訓練模型,如果你的batch比較大,那么你的學習率也要相應調大,不然模型收斂會很慢。
step_size
和 gamma
是學習率調度器的兩個參數。step_size
指每隔多少步或多少個epoch調整一次學習率(你可以將步數調整到和訓練一個epoch所需要的步數一致),gamma 是調整的倍率,比如每隔7個epoch,學習率會乘以
0.1`。
num_classes
表示有兩個分類類別,比如我的這個例子中所用到的“真假新聞”。
grad_clip
是一個布爾值,決定是否對梯度進行裁剪。如果梯度爆炸或者訓練不穩定,可以打開這個選項,不過我這里設備夠了,所以它是關閉狀態。
train_path
、valid_path
和 test_path
是數據集的路徑,分別對應訓練集、驗證集和測試集。
下面來看看代碼,整體代碼并不多,你甚至可以把它當作一個模板,以后有類似的任務就可以拿他們出來魔改,首先我們先來看一下數據長什么樣:
這個數據格式并不是很好,ID列
或者說索引列
都沒有做好名稱設置,至于title
和text
則是新聞的標題和正文,label
是標簽列,1代表新聞是真新聞,0代表新聞是假新聞。
整個數據集將訓練集劃分為24353行,驗證集和測試集都劃分為8117行,不過里面有一些數據是壞的,正常讀沒法讀出來,所以只能加上on_bad_lines='skip'
,這點在后面用的時候要注意。下面先來講講Dataprocess里面每一個函數的作用:
def collate_fn(batch):
"""
用于動態padding和batch數據的處理。
"""
inputs, labels = zip(*batch) # 分離輸入和標簽
# 獲取 tokenizer 的 keys,例如 'input_ids', 'attention_mask'
input_keys = inputs[0].keys()
# 動態 padding 對每個 key 的張量進行對齊
batch_inputs = {
key: pad_sequence([inp[key].squeeze(0) for inp in inputs], batch_first=True, padding_value=0)
for key in input_keys
}
# 將標簽堆疊
batch_labels = torch.stack(labels)
return batch_inputs, batch_labels
先來講講數據處理部分,這段代碼主要是用來處理批量數據加載的問題,特別是處理那些輸入長度不一的數據。我們通過這個叫 collate_fn
的函數來動態地“整理”這些數據,確保每次輸入到模型里的數據格式都是一致的。
假設你的數據是個列表,每個元素是一個(輸入, 標簽)
對。inputs,labels=zip(*batch)
這一行就是把它們分成兩個單獨的部分:inputs
是所有輸入數據,labels
是所有標簽,這個相信大家都不陌生。假設你的batch站這樣:
batch = [({'input_ids': [1, 2, 3]}, 0), ({'input_ids': [4, 5]}, 1)]
那么zip之后就長這樣:
inputs = ({'input_ids': [1, 2, 3]}, {'input_ids': [4, 5]})
labels = (0, 1)
而很多時候,每個輸入數據的長度可能不一樣,比如這個例子中新聞標題+文本的長度。我們需要“補齊”它們,這樣才能放進一個統一的張量里,這就是動態補齊
,這里用的是 pad_sequence
方法。
input_keys=inputs[0].keys()
先獲取輸入字典的所有key,比如input_ids
或attention_mask
。假設輸入有兩句話:
第一句:[1, 2, 3]
第二句:[4, 5]
Padding之后就變成下面這樣,對齊了維度:
[[1, 2, 3],
[4, 5, 0]]
而標簽呢沒有長度上的不一致問題,自然不需要padding,只需要把它們按批次組合成一個張量供后續計算就行,不需要像輸入那樣動態補齊,直接用 torch.stack
把它們變成一個張量就好。
這個 collate_fn
的核心任務就是動態補齊數據,讓模型可以接受批量數據輸入。如果你輸入的數據格式不對,或者有些必要的key缺失,那它就無法正常工作,所以在你使用之前要確保數據的完整性。
def prepare_input(cfg, text):
tokenizer = AutoTokenizer.from_pretrained(cfg.tokenizer_path)
inputs = tokenizer(text, return_tensors='pt', truncation=True, padding=True, max_length=cfg.max_length)
return inputs
這段代碼的主要功能是把一段文本轉化成模型可以處理的輸入格式。簡單來說,它是用來把“人話”變成“機器能懂的話”,而這個過程主要是靠分詞器(tokenizer)來完成的。
首先看第一行,tokenizer = AutoTokenizer.from_pretrained(cfg.tokenizer_path)
,這里是從預訓練的分詞器中加載一個模型。我們通過 cfg.tokenizer_path
指定了分詞器的位置,這里我在config文件中配置的是ModernBert-base和Bert-base的路徑,這一步相當于拿到了一個“文本翻譯工具”。
接著,inputs = tokenizer(...)
這一步是翻譯的關鍵。我們把原始文本text
丟給分詞器,讓它做幾件事:
[CLS]
或[SEP]
這樣的特殊符號會加到句子的開頭或結尾,幫助模型理解句子的邊界。cfg.max_length
),會被裁剪掉,確保不會超出模型的輸入限制。cfg.max_length
這么長,保證所有輸入的長度一致。return_tensors='pt'
),這樣就可以直接送到模型里。最后,處理完的 inputs
就是一個包含多個鍵值對的字典,比如:
input_ids
:分詞后的 ID。attention_mask
:用來告訴模型哪些位置是填充的,哪些是有效內容。舉個例子,假設你的 cfg.max_length=10
,文本是 "I love coding"
。分詞器處理后的結果可能是:
{
'input_ids':[[101,1045,2293,13639,102,0,0,0,0,0]],
'attention_mask':[[1,1,1,1,1,0,0,0,0,0]]
}
這里 101
和 102
是特殊符號[CLS]
和 [SEP]
,中間是單詞的 ID,最后幾個零是補的。
文本內容明確,分詞器路徑有效,比如:
cfg = {'tokenizer_path': 'bert-base-uncased', 'max_length': 10}
text = "I love coding"
這會正常輸出一個包含input_ids
和attention_mask
的字典。
當然如果你的配置文件有問題,比如cfg.tokenizer_path
指向了不存在的路徑,或者cfg.max_length
沒有設置。這種情況下,代碼可能會報錯,比如:
OSError: Can't load tokenizer for path: ...
或者如果文本太長而沒有設置 truncation=True
,模型會因為輸入過長而無法處理,不過modernbert有8k的限制,比起bert的512長太多了,長度沒啥問題。
class FakeNewsTrainDataset(Dataset):
def __init__(self, cfg, df):
self.cfg = cfg
self.titles = df['title'].values
self.texts = df['text'].values
self.contents = self.titles + self.texts
self.labels = df[cfg.target_cols].values
def __len__(self):
return len(self.labels)
def __getitem__(self, item):
inputs = prepare_input(self.cfg, self.contents[item])
label = torch.tensor(self.labels[item], dtype = torch.float)
return inputs, label
然后我們來看這個FakeNewsTrainDataset
類,這是PyTorch里用來處理數據集的標準模板。一開始,__init__
方法負責初始化數據集,類似于“開場準備”。
然后我們傳入配置 cfg
和數據表 df
,把標題(title
)和正文(text
)從數據表中提取出來,然后我們把標題和正文拼接在一起,存到self.contents
,這樣每條數據就是完整的新聞內容。同時把對應的標簽列,存到self.labels
。
接著,__len__
方法告訴我們這個數據集有多少條數據,就是標簽的數量。比如如果有1000篇新聞,這里會返回1000。
最后,這個__getitem__
方法是核心,用來根據索引取出一條數據。在我定義的這個類里面的具體操作是這樣的:
prepare_input
函數處理新聞內容,把原始文本轉化為模型輸入格式,模型能理解的格式。這里傳入的內容是self.contents[item]
,也就是第item
條新聞。torch.tensor(self.labels[item], dtype=torch.float)
確保標簽是浮點數格式,方便后續計算。每次返回的就是一個 (inputs, label)
二元組,inputs
是模型的輸入,label
是真實的答案。
舉個例子,假設數據表df
是這樣的:
df = pd.DataFrame({
'title':['Breaking News!','Tech Update'],
'text':['Aliens landed on Earth.','New AI model released.'],
'label':[0,1]
})
cfg = {'target_cols':'label'}
對于第 0 條數據,__getitem__(0)
會返回:
inputs = {'input_ids': [[101, 2924, 3246, ...]], 'attention_mask': [[1, 1, 1, ...]]} # 經過分詞和編碼的新聞內容
label = tensor(0.0) # 假新聞標簽
??注意:在實際打比賽或者其他的測試任務里,在沒有標簽的情況下,這里應該再定義一個類:
class FakeNewsTestDataset(Dataset):
def __init__(self, cfg, df):
self.cfg = cfg
self.titles = df['title'].values
self.texts = df['text'].values
self.contents = self.titles + self.texts
# self.labels = df[cfg.target_cols].values # 關于label的都要刪除掉
def __len__(self):
return len(self.labels)
def __getitem__(self, item):
inputs = prepare_input(self.cfg, self.contents[item])
# label = torch.tensor(self.labels[item], dtype = torch.float)
return inputs
池化
# 平均池化層
class MeanPooling(nn.Module):
def __init__(self):
super(MeanPooling, self).__init__()
def forward(self, last_hidden_state, attention_mask):
input_mask_expanded = attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float()
sum_embeddings = torch.sum(last_hidden_state * input_mask_expanded, 1)
sum_mask = input_mask_expanded.sum(1)
sum_mask = torch.clamp(sum_mask, min = 1e-9)
mean_embeddings = sum_embeddings/sum_mask
return mean_embeddings
# 最大池化層
class MaxPooling(nn.Module):
def __init__(self):
super(MaxPooling, self).__init__()
def forward(self, last_hidden_state, attention_mask):
input_mask_expanded = attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float()
embeddings = last_hidden_state.clone()
embeddings[input_mask_expanded == 0] = -1e4
max_embeddings, _ = torch.max(embeddings, dim = 1)
return max_embeddings
我在poolings文件里頭寫了四種不同的池化方法,每種方法的作用是把輸入的隱藏狀態(last_hidden_state
)壓縮成一個固定大小的向量,用來表示整個序列的語義信息。池化的好處是將每個序列的變長特征統一成固定大小的向量,讓模型更容易處理。
平均池化層的邏輯是對序列的每個位置求平均值。我們用 attention_mask
把無效的部分過濾掉,然后對有效位置的隱藏狀態取平均。假如我們輸入一個句子,它的 last_hidden_state
是一個 [4, 768]
的張量,表示有 4 個 token,每個 token 的向量大小是 768。通過平均池化后,輸出會變成 [1, 768]
,即用每個 token 的特征向量的平均值表示整句話。
最大池化層稍微復雜一點。它取序列每個位置上的最大值。實現上,我們用 attention_mask
來擴展輸入,然后把無效位置的值設成一個很小的數(-1e4),確保這些位置不會影響最大值計算。如果輸入是 [4, 768]
的張量,最大池化會找出每一列的最大值作為結果。
# 最小池化層
class MinPooling(nn.Module):
def __init__(self):
super(MinPooling, self).__init__()
def forward(self, last_hidden_state, attention_mask):
input_mask_expanded = attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float()
embeddings = last_hidden_state.clone()
embeddings[input_mask_expanded == 0] = 1e-4
min_embeddings, _ = torch.min(embeddings, dim = 1)
return min_embeddings
最小池化層的邏輯和最大池化類似,只是它找的是最小值。我們會把無效位置的值設成一個很大的數(1e4),然后取每列的最小值。這種方法通常在特定任務中才用,畢竟最小值并不總是能很好地表示序列的語義。
# 注意力池化層
class AttentionPooling(nn.Module):
def __init__(self, in_dim):
super().__init__()
self.attention = nn.Sequential(
nn.Linear(in_dim, in_dim),
nn.LayerNorm(in_dim),
nn.GELU(),
nn.Linear(in_dim, 1),
)
def forward(self, last_hidden_state, attention_mask):
w = self.attention(last_hidden_state).float()
w[attention_mask==0]=float('-inf')
w = torch.softmax(w,1)
attention_embeddings = torch.sum(w * last_hidden_state, dim=1)
return attention_embeddings
注意力池化層就復雜一點了。它引入了一個自定義的注意力機制,用來動態調整每個位置的重要性。我們先通過一個全連接網絡為每個 token 生成權重 w
,然后用 softmax 歸一化權重,確保總和為 1。接著用這些權重加權隱藏狀態,得到最終的序列表示。比如,如果輸入句子中“關鍵字”的權重比較高,最終的表示會更側重于這些關鍵字的特征。
舉個例子,假如我們有一個句子“這是一條假新聞”。分詞后有 6 個 token,它的隱藏狀態是 [6, 768]
,注意力掩碼是 [1, 1, 1, 1, 1, 1]
,表示每個位置都有效。對于平均池化,結果會是簡單平均值;對于最大池化,可能更側重于“假”和“新聞”的特征;對于注意力池化,如果權重主要集中在“假新聞”,結果就會特別強調這些詞的信息。
不同的池化方法適合不同的任務。比如,平均池化簡單直接,適合泛化任務;最大池化適合強調最強特征;注意力池化則更靈活,可以動態選擇重點,但計算成本稍高。如果方法選得不對,比如在句子中重點詞被弱化,模型效果可能會打折扣。
這里我用的是自定義模型,有人可能會疑惑我為什么不直接用AutoModelForSequenceClassification
,因為我這里是用的是自定義池化操作,在前面我也有提到,你甚至可以將這一份代碼當作一份模板,隨時可以取來用并且靈活修改,如果你的過程中需要用到什么自定義的操作,自己修改自己運行即可。
# 定義模型
class FakeNewsModel(nn.Module):
def __init__(self, CFG):
super().__init__()
self.CFG = CFG
self.tokenizer = AutoTokenizer.from_pretrained(CFG.model_name)
self.config = AutoConfig.from_pretrained(CFG.model_name, ouput_hidden_states=True)
self.model = AutoModel.from_pretrained(CFG.model_name, config=self.config)
# 自定義池化層
if CFG.pooling == 'mean':
self.pool = MeanPooling()
elif CFG.pooling == 'max':
self.pool = MaxPooling()
elif CFG.pooling == 'min':
self.pool = MinPooling()
elif CFG.pooling == 'attention':
self.pool = AttentionPooling(self.config.hidden_size)
else:
raise ValueError(f"Unsupported pooling type: {CFG.pooling}")
# 分類頭部
self.fc = nn.Linear(self.model.config.hidden_size, CFG.num_classes)
def feature(self, inputs) -> torch.Tensor:
"""
提取隱藏層特征并進行池化。
"""
outputs = self.model(**inputs)
# 使用最后一層的 hidden state(即 last_hidden_state)
last_hidden_state = outputs.last_hidden_state # [batch_size, seq_len, hidden_size]
attention_mask = inputs['attention_mask']
feature = self.pool(last_hidden_state, attention_mask) # 進行池化
return feature
def forward(self, inputs) -> torch.Tensor:
"""
前向傳播,輸出分類 logits。
"""
feature = self.feature(inputs)
logits = self.fc(feature)
return logits
我的這個自定義模型基于PyTorch的nn.Module
,并且用了預訓練的語言模型,我用的是bert,你也可以改為其他的模型。接下來我們逐步拆解一下。
初始化部分首先加載了分詞器、配置和預訓練模型。
if CFG.pooling =='mean':
self.pool =MeanPooling()
elif CFG.pooling =='max':
self.pool =MaxPooling()
elif CFG.pooling =='min':
self.pool =MinPooling()
elif CFG.pooling =='attention':
self.pool =AttentionPooling(self.config.hidden_size)
else:
raiseValueError(f"Unsupported pooling type: {CFG.pooling}")
接下來定義了一個自定義的池化層,這里我是設置了有4種不同的池化方式,如果你有其他的池化方式也可以加在Pooling.py文件里面:
mean
),就是取每個序列位置的平均值,得到句子的整體表示。max
),取每個位置上的最大值。min
),取每個位置上的最小值。attention
),用注意力機制動態地決定每個位置的重要性。最后的分類層 self.fc
是一個全連接層,它的輸入是模型提取出來的特征,輸出的維度是分類類別數(比如真假新聞,2 類)。
feature
函數是用來提取特征的。它先通過self.model
獲取每個token的隱藏狀態(last_hidden_state
),這是模型理解輸入文本后的結果。然后結合注意力掩碼attention_mask
進行池化,把序列壓縮成一個固定大小的特征向量。
forward
函數是模型的前向傳播邏輯。它調用feature
提取特征,再通過分類層計算出分類結果logits
,每個類別對應一個得分。
舉個例子,你輸入一個新聞文本
inputs = {'input_ids': [[101, 2003, 2023, 1037, 2173, 102]], 'attention_mask': [[1, 1, 1, 1, 1, 1]]}
這里的 input_ids
是分詞后的 ID,attention_mask
表示哪些位置有效。經過feature
提取特征后,可能得到一個形狀為 [batch_size, hidden_size]
的向量,比如[1, 768]
。然后分類層輸出形狀為[1, 2]
,兩個得分表示“假新聞”和“真新聞”的置信度。
模型訓練方面我沒有一開始就初始化整個模型的參數,畢竟這也只是個簡單的分類任務,預訓練模型的性能加上微調就已經足夠了。
def train(cfg, df1, df2):
# 加載數據
train_dataset = FakeNewsTrainDataset(cfg, df1)
valid_dataset = FakeNewsTrainDataset(cfg, df2)
train_loader = DataLoader(train_dataset, batch_size=cfg.batch_size, shuffle=True, collate_fn=collate_fn)
valid_loader = DataLoader(valid_dataset, batch_size=cfg.batch_size, shuffle=True, collate_fn=collate_fn)
# 加載模型
model = FakeNewsModel(cfg)
model.to(cfg.device)
# 加載優化器
optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.learning_rate)
# 加載學習率調度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=cfg.step_size, gamma=cfg.gamma)
# 訓練模型
best_val_loss = float('inf')
for epoch in range(cfg.epochs):
# Training
model.train()
train_loss = 0.0
correct = 0
total = 0
# tqdm 顯示訓練進度
loop = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{cfg.epochs}", leave=False)
for inputs, labels in loop:
inputs = {key: value.to(cfg.device) for key, value in inputs.items()}
labels = labels.to(cfg.device)
# 如果標簽是 one-hot 編碼,需要用argmax獲取類索引
if len(labels.shape) > 1:
labels = labels.argmax(dim=1)
# 前向傳播
outputs = model(inputs)
loss = F.cross_entropy(outputs, labels)
# 反向傳播
loss.backward()
# 梯度裁剪(防止梯度爆炸)
if cfg.grad_clip:
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip)
optimizer.step()
optimizer.zero_grad()
# 累計損失
train_loss += loss.item() * cfg.batch_size
# 計算準確率
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 更新 tqdm 描述
loop.set_postfix(loss=loss.item(), acc=100.0 * correct / total)
# 學習率調度器步進
if scheduler:
scheduler.step()
# 訓練集平均損失和準確率
train_loss /= len(train_loader.dataset)
train_acc = 100.0 * correct / total
# 驗證模型
val_loss, val_acc = validate(cfg, model, valid_loader)
# 保存最佳模型
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), cfg.best_model_path)
# 打印每個 epoch 的結果
print(f"Epoch {epoch + 1}/{cfg.epochs}")
print(f" Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}%")
print(f" Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.2f}%")
我們一步步來拆解,讓你明白每一部分都在干嘛。
首先,我們需要把數據準備好。這里有兩個數據集,一個是訓練集 df1
,另一個是驗證集 df2
。每個數據集都通過 FakeNewsTrainDataset
類處理后,變成了適合加載器使用的對象train_dataset
和 valid_dataset
。然后我們用DataLoader
把它們包裝起來,這樣就能批量讀取數據。每次讀取一批的大小是 cfg.batch_size
(比如 8 或 16),而且訓練數據會被隨機打亂,這對模型的泛化能力很重要。
接下來是模型部分。我們用上面我們自定義的FakeNewsModel
。然后,我們需要一個優化器來調整模型的參數,這里用的是AdamW
,能幫助模型更快收斂。還加了一個學習率調度器 StepLR
,它會在每隔一定的訓練步長(step_size
)后,把學習率按照衰減因子(gamma
)降低,幫助模型更穩地學習。
訓練過程是核心部分,分成多個輪次(epochs
)。每一輪中,我們會把整個訓練集跑一遍,同時在驗證集上測試模型的表現。
在訓練時,我們會讓模型進入“訓練模式”(model.train()
),它會啟用一些特定的機制,比如 Dropout(用來隨機忽略部分神經元,防止過擬合),如果你不想dropout,可以在定義模型的時候,設置:
self.config.hidden_dropout = 0.
self.config.hidden_dropout_prob = 0.
self.config.attention_dropout = 0.
self.config.attention_probs_dropout_prob = 0.
然后,我們開始遍歷訓練數據,用tqdm
顯示進度條,這樣我們能實時看到訓練的進度。
每次處理一批數據,輸入和標簽都會被放到 GPU 或 CPU 上。如果標簽是 one-hot 編碼,我們先用 argmax
轉成分類索引,這樣計算交叉熵損失(F.cross_entropy
)時不會報錯。模型會通過輸入數據生成輸出,然后計算損失值。損失值越小,說明模型表現越好。
接著是反向傳播,loss.backward()
會計算梯度,讓模型知道每個參數該怎么調整。為了防止梯度值太大導致模型不穩定,我們可以用梯度裁剪(clip_grad_norm_
)限制梯度的大小。最后一步是優化器更新參數,并清空累積的梯度。
在訓練中,我們還會統計損失值和準確率。通過比較模型的預測值和真實標簽,我們能知道預測對了多少個,占總數的百分比就是準確率。
當一輪訓練結束后,我們會用驗證集測試模型,看看它對沒見過的數據表現如何。驗證部分會用 validate
函數完成。這里的損失和準確率是我們評估模型的關鍵指標。
如果發現這一輪的驗證損失比之前最低的還小,就說明模型變得更好了。我們把這個“最好”的模型存起來,文件名保存在 cfg.best_model_path
。
最后,每輪結束后都會打印一份報告,告訴我們這輪的訓練損失和準確率,還有驗證損失和準確率。這些信息能幫我們判斷模型是否在正常學習,還是出現了過擬合(驗證損失升高)。
用bert訓練的效果如下(1epoch,512 max_len):
用modernbert的效果如下(1epoch,1024 max_len, 能更長,效果能更好):
這個訓練過程非常常見,但里面有許多細節可以優化,比如選擇不同的優化器、調整學習率調度器的參數、或者嘗試更多輪次的訓練來找到最好的模型等。
def validate(cfg, model, val_loader):
"""
驗證模型性能。
參數:
- cfg: 配置對象
- model: PyTorch 模型
- val_loader: 驗證數據加載器
返回:
- val_loss: 驗證集平均損失
- val_acc: 驗證集準確率
"""
model.eval()
val_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in val_loader:
inputs = {key: value.to(cfg.device) for key, value in inputs.items()}
labels = labels.to(cfg.device)
if len(labels.shape) > 1:
labels = labels.argmax(dim=1)
outputs = model(inputs)
loss = F.cross_entropy(outputs, labels)
# 累計損失
val_loss += loss.item() * cfg.batch_size
# 計算準確率
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
# 平均損失和準確率
val_loss /= len(val_loader.dataset)
val_acc = 100.0 * correct / total
return val_loss, val_acc
model = FakeNewsModel(CFG)
model.load_state_dict(torch.load(CFG.best_model_path))
model.to(CFG.device)
model.eval()
test_df = pd.read_csv(CFG.test_path, on_bad_lines='skip', sep=';').sample(100)
test_dataset = FakeNewsTrainDataset(CFG, test_df)
test_loader = DataLoader(test_dataset, batch_size=CFG.batch_size, shuffle=False, collate_fn=collate_fn)
test_loss, test_acc = validate(CFG, model, test_loader)
print(f"Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.2f}%")
這段代碼的目的是驗證模型的性能,看看它在驗證數據集上的表現怎么樣。我們主要關注兩件事:平均損失和準確率。
代碼的第一步是讓模型進入“評估模式”(model.eval()
)。評估模式和訓練模式不同,它會關閉一些訓練時才需要的功能,比如 Dropout。這一步很重要,因為評估的時候,我們希望模型的行為盡可能穩定。
接著,torch.no_grad()
會告訴PyTorch在這段代碼里不用計算梯度。這是為了節省內存和計算資源,因為驗證時我們不需要更新模型參數。
然后就是處理驗證數據。val_loader
是驗證數據加載器,會一批一批地喂數據給模型。每一批包括輸入 inputs
和標簽 labels
。這些數據會被送到 GPU 或 CPU 上,具體看你的設備配置。
如果標簽是one-hot編碼(比如[0, 1]
代表第1類,[1, 0]代表第0類),我們用argmax
把它轉成類索引(比如 1
或0
)。這樣模型輸出的預測值可以直接和真實標簽進行比較。
模型會根據輸入生成輸出 outputs
,我們用交叉熵損失函數(F.cross_entropy
)計算預測結果和真實標簽之間的誤差,這就是每一批數據的損失。
我們還要計算模型的準確率。outputs.max(1)
會找到每一行(也就是每個樣本)預測值最大的那個類別,這就是模型的預測結果。接著,我們用 predicted.eq(labels)
比較預測和真實標簽,看預測對了多少個。累加所有批次的數據后,用對的樣本數除以總樣本數,就得到了驗證集的準確率。
最后,我們計算驗證集的平均損失。驗證集的總損失除以樣本數量,就是平均損失。準確率則是用百分比表示,方便觀察。
比如說,如果驗證集有1000個樣本,模型預測對了850個,那么準確率就是85%。如果平均損失值是0.2,說明模型的預測結果和真實標簽之間的誤差比較小。
這段代碼會返回兩個值,一個是驗證集的平均損失(val_loss
),另一個是驗證集的準確率(val_acc
)。我們可以用它們來判斷模型在驗證數據上的表現。如果驗證損失和準確率都很理想,說明模型學到了好的特征。如果驗證損失越來越高,準確率卻沒有提升,可能是模型過擬合了。
用bert的測試結果和modernbert的測試結果一樣,都是100%的準確率。
本文章轉載微信公眾號@Chal1ceAI