Files
Kaggle/gpt-chemistry/main.py
T

169 lines
5.4 KiB
Python
Raw Normal View History

# %%
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
import torch
import torch.nn as nn
import pandas as pd
from torch.utils.data import Dataset, DataLoader
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
get_cosine_schedule_with_warmup,
)
from tqdm import tqdm
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# %%
# 读取同文件夹下的对话 csv(列:问题、回答)
df = pd.read_csv("样本收集.csv", encoding="utf-8")
df = df.dropna(subset=["问题", "回答"]).reset_index(drop=True)
# 清洗空白字符:去首尾空白、合并连续空白为单个空格、替换中文全角空格
df["问题"] = df["问题"].astype(str).str.replace("\u3000", " ").str.replace(r"\s+", " ", regex=True).str.strip()
df["回答"] = df["回答"].astype(str).str.replace("\u3000", " ").str.replace(r"\s+", " ", regex=True).str.strip()
print(f"共 {len(df)} 条对话样本")
print(df.head())
# %%
# 加载 Qwen2.5-1.5B-Instructbf16 全量微调(单卡 16GB 够用)
model_name = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
trust_remote_code=True,
)
model.gradient_checkpointing_enable()
model.config.use_cache = False
model.to(device)
# 冻结前两层 decoder,节省显存
for i in range(2):
for p in model.model.layers[i].parameters():
p.requires_grad = False
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"可训练参数: {trainable}/{total} ({100*trainable/total:.2f}%)")
print(model)
# %%
# 对话数据集:把 (问题, 回答) 拼成提示文本,做因果语言建模
PROMPT_TEMPLATE = (
"你是一名材料科学助手,请根据问题给出准确、专业的回答。\n"
"问题:{q}\n"
"回答:{a}{eos}"
)
class ChatDataset(Dataset):
def __init__(self, dataframe, tokenizer, max_length=512):
self.df = dataframe.reset_index(drop=True)
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
q = str(self.df.loc[idx, "问题"])
a = str(self.df.loc[idx, "回答"])
text = PROMPT_TEMPLATE.format(q=q, a=a, eos=self.tokenizer.eos_token)
tokens = self.tokenizer(
text,
truncation=True,
max_length=self.max_length,
padding="max_length",
return_tensors="pt",
)
input_ids = tokens["input_ids"].squeeze(0)
attention_mask = tokens["attention_mask"].squeeze(0)
# pad 位置用 -100 屏蔽损失
labels = input_ids.clone()
labels[labels == self.tokenizer.pad_token_id] = -100
return input_ids, attention_mask, labels
def collate_fn(batch):
input_ids = torch.stack([b[0] for b in batch])
attention_mask = torch.stack([b[1] for b in batch])
labels = torch.stack([b[2] for b in batch])
return input_ids, attention_mask, labels
dataset = ChatDataset(df, tokenizer, max_length=512)
train_dataloader = DataLoader(
dataset, batch_size=1, shuffle=True, num_workers=2, collate_fn=collate_fn
)
# 抽样查看一条
sample = dataset[0]
print("sample input_ids shape:", sample[0].shape)
print("decoded:", tokenizer.decode(sample[0], skip_special_tokens=True))
# %%
# 训练配置
epochs = 30
learning_rate = 2e-5
warmup_ratio = 0.05
optimizer = torch.optim.AdamW(
[p for p in model.parameters() if p.requires_grad],
lr=learning_rate,
weight_decay=0.01,
)
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=int(warmup_ratio * epochs * len(train_dataloader)),
num_training_steps=epochs * len(train_dataloader),
)
best_loss = float("inf")
save_dir = "./output"
os.makedirs(save_dir, exist_ok=True)
# %%
# 训练循环
for epoch in range(epochs):
model.train()
training_loss = 0.0
progress_bar = tqdm(train_dataloader, desc=f"Epoch {epoch+1}/{epochs}")
lens = 0
for input_ids, attention_mask, labels in progress_bar:
input_ids = input_ids.to(device)
attention_mask = attention_mask.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
training_loss += loss.item()
lens += 1
progress_bar.set_postfix({
"loss": f"{loss.item():.4f}",
"avg_loss": f"{training_loss / lens:.4f}",
"lr": f"{scheduler.get_last_lr()[0]:.2e}",
})
scheduler.step()
avg_train_loss = training_loss / lens
print(f"Epoch {epoch+1} train_loss: {avg_train_loss:.4f}")
# 仅记录最优 loss,不每个 epoch 保存(1.5B 整存太费时费空间)
if avg_train_loss < best_loss:
best_loss = avg_train_loss
print(f"新最优 loss: {best_loss:.4f}")
# 训练结束保存整个最终模型
final_dir = os.path.join(save_dir, "final_model")
model.save_pretrained(final_dir)
tokenizer.save_pretrained(final_dir)
print(f"训练完成,最佳 loss: {best_loss:.4f}")
print(f"最终模型已保存到: {final_dir}")