首先我們先來看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_pathvalid_path 和 test_path 是數據集的路徑,分別對應訓練集、驗證集和測試集。

數據處理

下面來看看代碼,整體代碼并不多,你甚至可以把它當作一個模板,以后有類似的任務就可以拿他們出來魔改,首先我們先來看一下數據長什么樣:

這個數據格式并不是很好,ID列或者說索引列都沒有做好名稱設置,至于titletext則是新聞的標題和正文,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 方法。

假設輸入有兩句話:

第一句:[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丟給分詞器,讓它做幾件事:

  1. 1. 分詞:把整段文本拆成更小的單元,比如單詞或子詞。
  2. 2. 轉化成 ID:每個單詞或子詞都對應一個數字ID,比如 “hello” 可能變成101。
  3. 3. 加特殊符號:像[CLS][SEP]這樣的特殊符號會加到句子的開頭或結尾,幫助模型理解句子的邊界。
  4. 4. 截斷:如果文本太長(超過了 cfg.max_length),會被裁剪掉,確保不會超出模型的輸入限制。
  5. 5. 填充:如果文本太短,就在后面補零,補到 cfg.max_length 這么長,保證所有輸入的長度一致。
  6. 6. 返回張量:分詞結果會變成 PyTorch 的張量格式(return_tensors='pt'),這樣就可以直接送到模型里。

最后,處理完的 inputs 就是一個包含多個鍵值對的字典,比如:

舉個例子,假設你的 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_idsattention_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__ 方法是核心,用來根據索引取出一條數據。在我定義的這個類里面的具體操作是這樣的:

  1. 1. 用剛才說到的prepare_input函數處理新聞內容,把原始文本轉化為模型輸入格式,模型能理解的格式。這里傳入的內容是self.contents[item],也就是第item條新聞。
  2. 2. 把對應的標簽轉化成PyTorch張量,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文件里面:

  1. 1. 平均池化(mean),就是取每個序列位置的平均值,得到句子的整體表示。
  2. 2. 最大池化(max),取每個位置上的最大值。
  3. 3. 最小池化(min),取每個位置上的最小值。
  4. 4. 注意力池化(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把它轉成類索引(比如 10)。這樣模型輸出的預測值可以直接和真實標簽進行比較。

模型會根據輸入生成輸出 outputs,我們用交叉熵損失函數(F.cross_entropy)計算預測結果和真實標簽之間的誤差,這就是每一批數據的損失。

我們還要計算模型的準確率。outputs.max(1) 會找到每一行(也就是每個樣本)預測值最大的那個類別,這就是模型的預測結果。接著,我們用 predicted.eq(labels) 比較預測和真實標簽,看預測對了多少個。累加所有批次的數據后,用對的樣本數除以總樣本數,就得到了驗證集的準確率。

最后,我們計算驗證集的平均損失。驗證集的總損失除以樣本數量,就是平均損失。準確率則是用百分比表示,方便觀察。

比如說,如果驗證集有1000個樣本,模型預測對了850個,那么準確率就是85%。如果平均損失值是0.2,說明模型的預測結果和真實標簽之間的誤差比較小。

這段代碼會返回兩個值,一個是驗證集的平均損失(val_loss),另一個是驗證集的準確率(val_acc)。我們可以用它們來判斷模型在驗證數據上的表現。如果驗證損失和準確率都很理想,說明模型學到了好的特征。如果驗證損失越來越高,準確率卻沒有提升,可能是模型過擬合了。

用bert的測試結果和modernbert的測試結果一樣,都是100%的準確率。

本文章轉載微信公眾號@Chal1ceAI

上一篇:

Vue3.4+Element-plus+Vite通用后臺管理系統

下一篇:

Go高性能JSON庫:Sonic
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費