1041 lines
36 KiB
Python
1041 lines
36 KiB
Python
from pathlib import Path
|
||
import sys
|
||
|
||
project_dir = Path(__file__).parent.parent
|
||
project_path = project_dir.as_posix()
|
||
if project_path not in sys.path:
|
||
sys.path.insert(0, project_path)
|
||
|
||
import argparse
|
||
from collections import Counter
|
||
from contextlib import nullcontext
|
||
from dataclasses import asdict, dataclass
|
||
import json
|
||
import os
|
||
import re
|
||
import time
|
||
from typing import List, Literal, Union
|
||
import warnings
|
||
|
||
import datasets
|
||
import numpy as np
|
||
import pandas as pd
|
||
import torch
|
||
from torch import nn, optim
|
||
import torch.distributed as dist
|
||
from torch.nn import functional as F
|
||
from torch.nn.parallel import DistributedDataParallel
|
||
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
||
from tqdm import tqdm, trange
|
||
from transformers import (
|
||
AutoModel,
|
||
AutoModelForCausalLM,
|
||
AutoModelForSequenceClassification,
|
||
AutoTokenizer,
|
||
BatchEncoding,
|
||
BertModel,
|
||
BertTokenizerFast,
|
||
PreTrainedModel,
|
||
PreTrainedTokenizerBase,
|
||
TextStreamer,
|
||
)
|
||
import wandb
|
||
|
||
from model.model_lora import apply_lora, load_lora
|
||
from model.model_minimind import MiniMindConfig
|
||
from sentence_transformers import SentenceTransformer
|
||
from trainer.trainer_utils import (
|
||
Logger,
|
||
MiniMindForCausalLM,
|
||
SkipBatchSampler,
|
||
init_distributed_mode,
|
||
is_main_process,
|
||
lm_checkpoint,
|
||
setup_seed,
|
||
)
|
||
|
||
warnings.filterwarnings("ignore")
|
||
|
||
|
||
# ----- dataset utils -----
|
||
|
||
|
||
def save_jsonl(lines: Union[list[dict], pd.DataFrame], path, overwrite=False):
|
||
if isinstance(lines, pd.DataFrame):
|
||
lines = lines.to_dict(orient="records")
|
||
|
||
path = Path(path)
|
||
if not overwrite:
|
||
assert not path.exists()
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(path, "w") as f:
|
||
for i, line in enumerate(lines):
|
||
json_line = json.dumps(line, ensure_ascii=False, separators=(",", ":"))
|
||
if i == 0:
|
||
f.write(json_line)
|
||
else:
|
||
f.write("\n" + json_line)
|
||
print(f"Saved {len(lines)} lines to {path.as_posix()}")
|
||
|
||
|
||
def load_jsonl(path):
|
||
data = pd.read_json(path_or_buf=path, lines=True)
|
||
return data
|
||
|
||
|
||
def load_dataset(
|
||
dataset_name="ChineseSquad",
|
||
max_query_len=64,
|
||
max_passage_len=128,
|
||
split=True,
|
||
):
|
||
if dataset_name == "ChineseSquad":
|
||
dataset_name = "lighteval/ChineseSquad"
|
||
elif dataset_name == "natural-questions":
|
||
dataset_name = "sentence-transformers/natural-questions"
|
||
else:
|
||
raise NotImplementedError
|
||
|
||
dataset = datasets.load_dataset(dataset_name)
|
||
if dataset_name == "sentence-transformers/natural-questions":
|
||
dataset = dataset.filter(
|
||
lambda x: len(x["query"]) <= max_query_len
|
||
and len(x["answer"]) <= max_passage_len
|
||
)
|
||
if split:
|
||
dataset = dataset["train"].train_test_split(test_size=0.05, seed=42)
|
||
else:
|
||
dataset = dataset.filter(
|
||
lambda x: len(x["question"]) <= max_query_len
|
||
and len(x["context"]) <= max_passage_len
|
||
)
|
||
return dataset
|
||
|
||
|
||
class DPRDataset(Dataset):
|
||
def __init__(
|
||
self,
|
||
dataset: datasets.Dataset,
|
||
tokenizer: PreTrainedTokenizerBase,
|
||
max_query_len=64,
|
||
max_passage_len: int = 128,
|
||
):
|
||
super().__init__()
|
||
self.tokenizer = tokenizer
|
||
self.max_query_len = max_query_len
|
||
self.max_passage_len = max_passage_len
|
||
self.dataset = dataset
|
||
if "query" in self.dataset.features:
|
||
self.dataset_name = "nq"
|
||
else:
|
||
self.dataset_name = "cs"
|
||
|
||
def __len__(self):
|
||
return len(self.dataset)
|
||
|
||
def __getitem__(self, index):
|
||
sample = self.dataset[index]
|
||
|
||
def encode(key, max_length):
|
||
if isinstance(self.tokenizer, BertTokenizerFast):
|
||
text = sample[key]
|
||
else:
|
||
text = self.tokenizer.bos_token + sample[key] + self.tokenizer.eos_token
|
||
inputs = self.tokenizer(
|
||
text,
|
||
max_length=max_length,
|
||
truncation=True,
|
||
return_tensors="pt",
|
||
padding_side="right",
|
||
padding="max_length",
|
||
return_token_type_ids=False,
|
||
)
|
||
inputs["input_ids"] = inputs["input_ids"].view(-1)
|
||
inputs["attention_mask"] = inputs["attention_mask"].view(-1)
|
||
return inputs
|
||
|
||
if self.dataset_name == "cs": # ChineseSquad
|
||
query_inputs = encode("question", self.max_query_len)
|
||
passage_inputs = encode("context", self.max_passage_len)
|
||
else: # natural-questions
|
||
query_inputs = encode("query", self.max_query_len)
|
||
passage_inputs = encode("answer", self.max_passage_len)
|
||
return query_inputs, passage_inputs
|
||
|
||
def tolist(self):
|
||
if self.dataset_name == "cs": # ChineseSquad
|
||
queries = list(self.dataset["question"])
|
||
passages = list(self.dataset["context"])
|
||
else:
|
||
queries = list(self.dataset["query"])
|
||
passages = list(self.dataset["answer"])
|
||
return queries, passages
|
||
|
||
|
||
# ----- retriever -----
|
||
|
||
|
||
class Encoder(nn.Module):
|
||
"""
|
||
Adapted from CMU-11-667 (https://cmu-llms.org/) EncoderModel
|
||
"""
|
||
|
||
def __init__(self, encoder: PreTrainedModel, temperature: float = 1.0):
|
||
super().__init__()
|
||
self.encoder = encoder
|
||
self.register_buffer("temperature", torch.tensor(temperature))
|
||
|
||
def forward(
|
||
self,
|
||
query_inputs: BatchEncoding = None,
|
||
passage_inputs: BatchEncoding = None,
|
||
):
|
||
q_embeddings = (
|
||
self.encode(query_inputs) if query_inputs else None
|
||
) # (n_queries, hidden_dim)
|
||
p_embeddings = (
|
||
self.encode(passage_inputs) if passage_inputs else None
|
||
) # (n_passages, hidden_dim)
|
||
|
||
# for inference
|
||
if q_embeddings is None or p_embeddings is None:
|
||
return q_embeddings, p_embeddings
|
||
|
||
similarity = (
|
||
torch.matmul(q_embeddings, p_embeddings.T) / self.temperature
|
||
) # (n_queries, n_passages)
|
||
target = torch.arange(len(q_embeddings)).to(q_embeddings.device) # (n_queries,)
|
||
loss = F.cross_entropy(similarity, target)
|
||
return loss, similarity
|
||
|
||
def encode_text(
|
||
self,
|
||
texts: Union[str, List[str]],
|
||
tokenizer: PreTrainedTokenizerBase,
|
||
max_length=128,
|
||
):
|
||
if isinstance(texts, str):
|
||
texts = [texts]
|
||
texts = [tokenizer.bos_token + text + tokenizer.eos_token for text in texts]
|
||
inputs = tokenizer(
|
||
texts,
|
||
max_length=max_length,
|
||
truncation=True,
|
||
return_tensors="pt",
|
||
padding_side="right",
|
||
padding="max_length",
|
||
return_token_type_ids=False,
|
||
)
|
||
embeddings = self.encode(inputs.to(self.encoder.device))
|
||
return embeddings
|
||
|
||
def encode(self, inputs: BatchEncoding):
|
||
hidden_states = self.encoder(
|
||
**inputs
|
||
).last_hidden_state # (batch_size, seq_len, hidden_dim)
|
||
return self.pooling(hidden_states, inputs["attention_mask"])
|
||
|
||
def pooling(self, last_hidden_state, attention_mask):
|
||
"""
|
||
last_hidden_state: (batch_size, seq_len, hidden_dim)
|
||
attention_mask: (batch_size, seq_len)
|
||
embeddings: (batch_size, hidden_dim)
|
||
"""
|
||
if isinstance(self.encoder, BertModel):
|
||
last_hidden_state = last_hidden_state[:, 0, :] # (batch_size, hidden_dim)
|
||
else:
|
||
attention_mask = torch.where(
|
||
attention_mask == 1,
|
||
attention_mask
|
||
+ torch.arange(attention_mask.shape[1]).to(attention_mask.device),
|
||
0,
|
||
) # (batch_size, seq_len)
|
||
last_nonzero_indices = attention_mask.argmax(
|
||
dim=1, keepdim=True
|
||
) # (batch_size, 1)
|
||
last_token_indices = last_nonzero_indices.unsqueeze(-1).expand(
|
||
-1, -1, last_hidden_state.shape[-1]
|
||
) # (batch_size, 1, hidden_dim)
|
||
last_hidden_state = torch.gather(
|
||
last_hidden_state, 1, last_token_indices
|
||
) # (batch_size, 1, hidden_dim)
|
||
last_hidden_state = last_hidden_state.squeeze(1) # (batch_size, hidden_dim)
|
||
embeddings = F.normalize(last_hidden_state, p=2, dim=1)
|
||
return embeddings
|
||
|
||
|
||
@torch.inference_mode()
|
||
def similarity_bert(
|
||
model_bert: SentenceTransformer, queries: List[str], passages: List[str]
|
||
):
|
||
if isinstance(queries, str):
|
||
queries = [queries]
|
||
if isinstance(passages, str):
|
||
passages = [passages]
|
||
q_embeddings = model_bert.encode(queries) # (n_queries, hidden_dim)
|
||
p_embeddings = model_bert.encode(passages) # (n_passages, hidden_dim)
|
||
similarity = model_bert.similarity(
|
||
q_embeddings, p_embeddings
|
||
) # (n_queries, n_passages)
|
||
target = torch.arange(len(q_embeddings)).to(q_embeddings.device) # (n_queries,)
|
||
loss = F.cross_entropy(similarity, target)
|
||
return loss, similarity
|
||
|
||
|
||
@torch.inference_mode()
|
||
def calc_relative_advantage(similarity):
|
||
"""
|
||
similarity: (n_queries, n_passages)
|
||
"""
|
||
sorted_similarity = torch.sort(similarity, 1, descending=True)[0]
|
||
second_best = sorted_similarity[:, 1]
|
||
return (torch.diag(similarity) - second_best) / second_best
|
||
|
||
|
||
@torch.inference_mode()
|
||
def benchmark_retriever(
|
||
model: Union[Literal["bm25"], Encoder],
|
||
dataset: DPRDataset,
|
||
top_k=[5, 20, 40, 60, 80, 100],
|
||
batch_size=256,
|
||
):
|
||
if not np.iterable(top_k):
|
||
top_k = [top_k]
|
||
max_top_k = max(top_k)
|
||
top_k = sorted(top_k)
|
||
queries, passages = dataset.tolist()
|
||
unique_queries = []
|
||
unique_passages = []
|
||
for i in range(len(passages)):
|
||
if passages[i] not in unique_passages:
|
||
unique_queries.append(queries[i])
|
||
unique_passages.append(passages[i])
|
||
queries, passages = unique_queries, unique_passages
|
||
if isinstance(model, str):
|
||
if model == "bm25":
|
||
import bm25s
|
||
|
||
corpus_tokens = bm25s.tokenize(passages, stopwords="en")
|
||
retriever = bm25s.BM25(k1=0.9, b=0.4)
|
||
retriever.index(corpus_tokens)
|
||
query_tokens = bm25s.tokenize(queries)
|
||
indices = retriever.retrieve(query_tokens, k=max_top_k)[0]
|
||
else:
|
||
raise NotImplementedError
|
||
elif isinstance(model, Encoder):
|
||
loader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
|
||
all_q_embeddings = []
|
||
all_p_embeddings = []
|
||
device = model.encoder.device
|
||
for query_inputs, passage_inputs in tqdm(loader):
|
||
q_embeddings = model.encode(
|
||
query_inputs.to(device)
|
||
) # (batch_size, hidden_dim)
|
||
p_embeddings = model.encode(
|
||
passage_inputs.to(device)
|
||
) # (batch_size, hidden_dim)
|
||
all_q_embeddings.append(q_embeddings)
|
||
all_p_embeddings.append(p_embeddings)
|
||
q_embeddings = torch.cat(all_q_embeddings, dim=0) # (n_queries, hidden_dim)
|
||
p_embeddings = torch.cat(all_p_embeddings, dim=0) # (n_passages, hidden_dim)
|
||
similarity = q_embeddings @ p_embeddings.T # (n_queries, n_passages)
|
||
values, indices = torch.topk(similarity, k=max_top_k, dim=1)
|
||
indices = indices.cpu().numpy()
|
||
else: # sentence-transformers
|
||
similarity = similarity_bert(model, queries, passages)[1]
|
||
values, indices = torch.topk(similarity, k=max_top_k, dim=1)
|
||
indices = indices.cpu().numpy()
|
||
targets = np.arange(len(indices))[:, None]
|
||
accuracy = {}
|
||
for _top_k in top_k:
|
||
acc = np.any(indices[:, :_top_k] == targets, 1).mean().item()
|
||
accuracy[_top_k] = acc
|
||
# torch.cuda.empty_cache()
|
||
return accuracy
|
||
|
||
|
||
class BM25:
|
||
"""
|
||
bm25 = BM25(k1=0.9, b=0.4)
|
||
bm.index(corpus)
|
||
bm.retrieve(queries, top_k=100)
|
||
"""
|
||
|
||
def __init__(self, pattern=r"(?u)\b\w\w+\b", k1=0.9, b=0.4):
|
||
"""
|
||
The default `k1` and `b` are consistent with Karpukhin et al. (2020).
|
||
"""
|
||
self.pattern = re.compile(pattern)
|
||
self.k1 = k1
|
||
self.b = b
|
||
|
||
def split(self, text: str):
|
||
text = text.lower()
|
||
text = self.pattern.findall(text)
|
||
return text
|
||
|
||
def index(self, corpus: List[str]):
|
||
self.chunk_lens = []
|
||
self.freqs = []
|
||
token_to_num_chunks = Counter()
|
||
if isinstance(corpus, str):
|
||
corpus = [corpus]
|
||
for text in tqdm(corpus):
|
||
tokens = self.split(text)
|
||
chunk_len = len(tokens)
|
||
freqs = {}
|
||
for token in tokens:
|
||
if token not in freqs:
|
||
freqs[token] = 1
|
||
token_to_num_chunks[token] += 1
|
||
else:
|
||
freqs[token] += 1
|
||
self.chunk_lens.append(chunk_len)
|
||
self.freqs.append(freqs)
|
||
self.num_chunks = len(self.chunk_lens)
|
||
self.mean_chunk_len = np.mean(self.chunk_lens).item()
|
||
self.token_to_num_chunks = dict(token_to_num_chunks)
|
||
|
||
def retrieve(self, queries: List[str], top_k=100):
|
||
if isinstance(queries, str):
|
||
queries = [queries]
|
||
results = []
|
||
for query in tqdm(queries):
|
||
result = self._retrieve(query, top_k)
|
||
results.append(result)
|
||
indices = np.stack([result[0] for result in results])
|
||
scores = np.stack([result[1] for result in results])
|
||
return indices, scores
|
||
|
||
def _retrieve(self, query: str, top_k=100):
|
||
if top_k is None:
|
||
top_k = self.num_chunks
|
||
assert top_k <= self.num_chunks
|
||
query_tokens = self.split(query)
|
||
scores = np.zeros(self.num_chunks, dtype=float)
|
||
for chunk_index in range(self.num_chunks):
|
||
score = self.score(query_tokens, chunk_index)
|
||
scores[chunk_index] = score
|
||
indices = np.argsort(-scores)[:top_k]
|
||
scores = scores[indices]
|
||
return indices, scores
|
||
|
||
def score(self, query_tokens: List[str], chunk_index: int):
|
||
chunk_len = self.chunk_lens[chunk_index]
|
||
n = np.array(
|
||
list(
|
||
map(lambda token: self.token_to_num_chunks.get(token, 0), query_tokens)
|
||
)
|
||
)
|
||
freqs = np.array(
|
||
list(map(lambda token: self.freqs[chunk_index].get(token, 0), query_tokens))
|
||
)
|
||
IDF = self.calc_IDF(n)
|
||
weight = freqs / (
|
||
freqs + self.k1 * (1 - self.b + self.b * chunk_len / self.mean_chunk_len)
|
||
)
|
||
return np.sum(IDF * weight)
|
||
|
||
def calc_IDF(self, n: int):
|
||
"""
|
||
n: number of chunks containing the term
|
||
"""
|
||
return np.log((self.num_chunks - n + 0.5) / (n + 0.5) + 1)
|
||
|
||
|
||
# ----- train retriever -----
|
||
|
||
|
||
@dataclass
|
||
class TrainArgs:
|
||
save_dir: Union[str, Path] = project_dir / "checkpoints" # 模型保存目录
|
||
save_weight: str = "dpr" # 保存权重的前缀名
|
||
epochs: int = 1 # 训练轮数
|
||
batch_size: int = 32
|
||
learning_rate: float = 4e-6
|
||
weight_decay: float = 0.0
|
||
betas: tuple = (0.9, 0.999)
|
||
device: str = None
|
||
dtype: str = "bfloat16"
|
||
num_workers: int = 0 # 数据加载线程数
|
||
accumulation_steps: int = 1 # 梯度累积步数
|
||
grad_clip: float = 1.0 # 梯度裁剪阈值
|
||
log_interval: int = 100 # 日志打印间隔
|
||
save_interval: int = 100 # 模型保存间隔
|
||
hidden_size: int = 512 # 隐藏层维度
|
||
num_hidden_layers: int = 8 # 隐藏层数量
|
||
max_query_len: int = 64 # 问题最大长度
|
||
max_passage_len: int = 128 # 单个document的最大长度,对于`natural-questions`数据集,建议设置为512
|
||
use_amp: bool = None # 使用自动混合精度
|
||
temperature: float = 1.0
|
||
from_weight: str = "pretrain" # 基于哪个权重训练,为none则从头开始
|
||
from_resume: int = 0 # 是否自动检测&续训(0=否,1=是)
|
||
dataset: str = "ChineseSquad" # 训练数据集(同时也是检索数据集),`ChineseSquad`或`natural-questions`
|
||
use_wandb: bool = False
|
||
use_swanlab: bool = False # 使用swanlab时需要同时设置use_wandb=True, wandb会被替换为swanlab
|
||
train_bert: bool = False # 是否训练BERT-Mini,若是则加载BERT,则MiniMind模型和权重均失效
|
||
wandb_entity: str = None
|
||
wandb_project: str = "MiniMind"
|
||
|
||
def __post_init__(self):
|
||
assert self.dtype in ["bfloat16", "float16"]
|
||
assert self.dataset in ["ChineseSquad", "natural-questions"]
|
||
if self.device is None:
|
||
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
||
if self.use_amp is None:
|
||
self.use_amp = "cuda" in self.device
|
||
self.save_dir = Path(self.save_dir).as_posix()
|
||
if self.train_bert:
|
||
self.hidden_size = 256 # BERT-mini
|
||
self.model_dir = project_dir / "out"
|
||
if not self.model_dir.exists():
|
||
self.model_dir = project_dir / "checkpoints"
|
||
self.model_dir = self.model_dir.as_posix()
|
||
self.wandb = wandb
|
||
if self.use_swanlab:
|
||
import swanlab
|
||
|
||
self.wandb = swanlab
|
||
|
||
|
||
def get_num_params(model):
|
||
return sum(p.numel() for p in model.parameters() if p.requires_grad)
|
||
|
||
|
||
def init_model(
|
||
lm_config=None,
|
||
from_weight="pretrain",
|
||
tokenizer_path="../model",
|
||
save_dir="../out",
|
||
device="cuda",
|
||
bert=False,
|
||
eval_args=None,
|
||
):
|
||
if eval_args is not None:
|
||
tokenizer = AutoTokenizer.from_pretrained(eval_args.load_from)
|
||
if "model" in eval_args.load_from:
|
||
model = MiniMindForCausalLM(
|
||
MiniMindConfig(
|
||
hidden_size=eval_args.hidden_size,
|
||
num_hidden_layers=eval_args.num_hidden_layers,
|
||
use_moe=bool(eval_args.use_moe),
|
||
inference_rope_scaling=eval_args.inference_rope_scaling,
|
||
)
|
||
)
|
||
moe_suffix = "_moe" if eval_args.use_moe else ""
|
||
ckp = f"{eval_args.save_dir}/{eval_args.weight}_{eval_args.hidden_size}{moe_suffix}.pth"
|
||
model.load_state_dict(
|
||
torch.load(ckp, map_location=eval_args.device), strict=True
|
||
)
|
||
if eval_args.lora_weight != "None":
|
||
apply_lora(model)
|
||
load_lora(
|
||
model,
|
||
f"{eval_args.save_dir}/lora/{eval_args.lora_weight}_{eval_args.hidden_size}.pth",
|
||
)
|
||
else:
|
||
model = AutoModelForCausalLM.from_pretrained(
|
||
eval_args.load_from, trust_remote_code=True
|
||
)
|
||
else:
|
||
if bert:
|
||
model = AutoModel.from_pretrained("prajjwal1/bert-mini")
|
||
tokenizer = AutoTokenizer.from_pretrained("prajjwal1/bert-mini")
|
||
else:
|
||
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
|
||
model = MiniMindForCausalLM(lm_config)
|
||
model = Encoder(model, temperature=1.0)
|
||
|
||
if from_weight != "none":
|
||
moe_suffix = "_moe" if lm_config.use_moe else ""
|
||
weight_path = (
|
||
f"{save_dir}/{from_weight}_{lm_config.hidden_size}{moe_suffix}.pth"
|
||
)
|
||
weights = torch.load(weight_path, map_location=device)
|
||
model.load_state_dict(weights, strict=False)
|
||
|
||
Logger(
|
||
f"所加载Model可训练参数:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万"
|
||
)
|
||
return model.to(device), tokenizer
|
||
|
||
|
||
def train_epoch(
|
||
model: MiniMindForCausalLM,
|
||
args: TrainArgs,
|
||
lm_config: MiniMindConfig,
|
||
optimizer: optim.Optimizer,
|
||
scheduler: optim.lr_scheduler.LRScheduler,
|
||
scaler: torch.GradScaler,
|
||
epoch: int,
|
||
loader: DataLoader,
|
||
iters: int,
|
||
bar,
|
||
start_step=0,
|
||
use_wandb=False,
|
||
spend_time=0,
|
||
):
|
||
dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
|
||
grad_norm = float("nan")
|
||
adv = float("nan")
|
||
wandb = args.wandb
|
||
start_time = time.time()
|
||
|
||
def get_spend_time():
|
||
return spend_time + time.time() - start_time
|
||
|
||
for step, (query_inputs, passage_inputs) in enumerate(loader, start=start_step + 1):
|
||
query_inputs = query_inputs.to(args.device)
|
||
passage_inputs = passage_inputs.to(args.device)
|
||
|
||
with torch.autocast(device_type=args.device, enabled=args.use_amp, dtype=dtype):
|
||
loss, similarity = model(query_inputs, passage_inputs)
|
||
loss = loss / args.accumulation_steps
|
||
|
||
scaler.scale(loss).backward()
|
||
loss = loss.detach().cpu().numpy().item() * args.accumulation_steps
|
||
lr = optimizer.param_groups[-1]["lr"]
|
||
|
||
if step % args.accumulation_steps == 0:
|
||
scaler.unscale_(optimizer)
|
||
grad_norm = torch.nn.utils.clip_grad_norm_(
|
||
model.parameters(), args.grad_clip
|
||
)
|
||
grad_norm = grad_norm.detach().cpu().numpy().item()
|
||
|
||
scaler.step(optimizer)
|
||
scaler.update()
|
||
|
||
optimizer.zero_grad()
|
||
torch.cuda.empty_cache()
|
||
|
||
if use_wandb:
|
||
adv = (
|
||
calc_relative_advantage(similarity)
|
||
.mean()
|
||
.detach()
|
||
.cpu()
|
||
.numpy()
|
||
.item()
|
||
)
|
||
wandb.log(
|
||
{
|
||
"train_step": (epoch * len(loader) + step)
|
||
// args.accumulation_steps,
|
||
"train/epoch": epoch + 1,
|
||
"train/loss": loss,
|
||
"train/lr": lr,
|
||
"train/grad_norm": grad_norm,
|
||
"train/adv": adv,
|
||
"train/time": get_spend_time(),
|
||
}
|
||
)
|
||
scheduler.step()
|
||
if bar is not None:
|
||
bar.update()
|
||
bar.set_postfix_str(
|
||
f"[{epoch+1}/{args.epochs}]loss={loss:.4f},adv={adv:.4f},grad_norm={grad_norm:.4f}"
|
||
)
|
||
|
||
if step % args.log_interval == 0 or step == iters - 1:
|
||
Logger(f"Epoch:[{epoch+1}/{args.epochs},{step}/{iters}] loss:{loss:.4f}")
|
||
|
||
if (step % args.save_interval == 0 or step == iters - 1) and is_main_process():
|
||
model.eval()
|
||
ckp = f"{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}.pth"
|
||
if isinstance(model, torch.nn.parallel.DistributedDataParallel):
|
||
state_dict = model.module.state_dict()
|
||
else:
|
||
state_dict = model.state_dict()
|
||
state_dict = {k: v.half() for k, v in state_dict.items()} # 半精度保存
|
||
torch.save(state_dict, ckp)
|
||
wandb_id = None
|
||
if args.use_wandb:
|
||
wandb_id = getattr(wandb.run, "id", None)
|
||
lm_checkpoint(
|
||
lm_config,
|
||
weight=args.save_weight,
|
||
model=model,
|
||
optimizer=optimizer,
|
||
scaler=scaler,
|
||
scheduler=scheduler,
|
||
epoch=epoch,
|
||
step=step,
|
||
wandb_id=wandb_id,
|
||
save_dir=args.save_dir,
|
||
spend_time=get_spend_time(),
|
||
)
|
||
model.train()
|
||
return get_spend_time()
|
||
|
||
|
||
def train(
|
||
args: TrainArgs,
|
||
model: MiniMindForCausalLM = None,
|
||
tokenizer: PreTrainedTokenizerBase = None,
|
||
train_ds: DPRDataset = None,
|
||
early_return=False,
|
||
):
|
||
# ========== 1. 初始化环境和随机种子 ==========
|
||
local_rank = init_distributed_mode()
|
||
if dist.is_initialized():
|
||
args.device = f"cuda:{local_rank}"
|
||
setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))
|
||
|
||
# ========== 2. 配置目录、模型参数、检查ckp ==========
|
||
os.makedirs(args.save_dir, exist_ok=True)
|
||
lm_config = MiniMindConfig(
|
||
hidden_size=args.hidden_size,
|
||
num_hidden_layers=args.num_hidden_layers,
|
||
use_moe=False,
|
||
)
|
||
ckp_data = (
|
||
lm_checkpoint(lm_config, weight=args.save_weight, save_dir=args.save_dir)
|
||
if args.from_resume == 1
|
||
else None
|
||
)
|
||
|
||
# ========== 3. 设置混合精度 ==========
|
||
# set in TrainArgs
|
||
|
||
# ========== 4. 定义模型、数据、优化器 ==========
|
||
if model is None:
|
||
model, tokenizer = init_model(
|
||
lm_config,
|
||
args.from_weight,
|
||
tokenizer_path=project_dir / "model",
|
||
save_dir=args.model_dir,
|
||
device=args.device,
|
||
bert=args.train_bert,
|
||
)
|
||
# model = Encoder(model, temperature=args.temperature)
|
||
num_params = get_num_params(model)
|
||
if train_ds is None:
|
||
dataset = load_dataset(args.dataset, args.max_query_len, args.max_passage_len)
|
||
train_ds = DPRDataset(
|
||
dataset["train"],
|
||
tokenizer,
|
||
max_query_len=args.max_query_len,
|
||
max_passage_len=args.max_passage_len,
|
||
)
|
||
key = "validation" if args.dataset == "ChineseSquad" else "test"
|
||
test_ds = DPRDataset(
|
||
dataset[key],
|
||
tokenizer,
|
||
max_query_len=args.max_query_len,
|
||
max_passage_len=args.max_passage_len,
|
||
)
|
||
|
||
if early_return: # for debugging
|
||
return model, tokenizer, train_ds, test_ds
|
||
|
||
train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None
|
||
scaler = torch.GradScaler(args.device, enabled=(args.dtype == "float16"))
|
||
optimizer = optim.AdamW(
|
||
model.parameters(),
|
||
lr=args.learning_rate,
|
||
weight_decay=args.weight_decay,
|
||
betas=args.betas,
|
||
)
|
||
|
||
# ========== 5. 从ckp恢复状态 ==========
|
||
start_epoch, start_step = 0, 0
|
||
spend_time = 0
|
||
if ckp_data:
|
||
model.load_state_dict(ckp_data["model"])
|
||
optimizer.load_state_dict(ckp_data["optimizer"])
|
||
scaler.load_state_dict(ckp_data["scaler"])
|
||
start_epoch = ckp_data["epoch"]
|
||
start_step = ckp_data.get("step", 0)
|
||
spend_time = ckp_data.get("spend_time", 0)
|
||
|
||
if start_step > 0: # 第一个epoch且存在检查点
|
||
batch_sampler = SkipBatchSampler(
|
||
train_sampler or range(len(train_ds)), args.batch_size, start_step + 1
|
||
)
|
||
loader = DataLoader(
|
||
train_ds,
|
||
batch_sampler=batch_sampler,
|
||
num_workers=args.num_workers,
|
||
pin_memory=True,
|
||
)
|
||
else: # 默认从头开始
|
||
loader = DataLoader(
|
||
train_ds,
|
||
batch_size=args.batch_size,
|
||
shuffle=(train_sampler is None),
|
||
sampler=train_sampler,
|
||
num_workers=args.num_workers,
|
||
pin_memory=True,
|
||
)
|
||
iters = len(loader)
|
||
max_step = args.epochs * iters
|
||
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=max_step)
|
||
if start_step > 0:
|
||
scheduler.load_state_dict(ckp_data["scheduler"])
|
||
|
||
# ========== 6. DDP包模型 ==========
|
||
if dist.is_initialized():
|
||
model._ddp_params_and_buffers_to_ignore = {"freqs_cos", "freqs_sin"}
|
||
model = DistributedDataParallel(model, device_ids=[local_rank])
|
||
|
||
# ========== 7. 配wandb ==========
|
||
run = nullcontext()
|
||
use_wandb = args.use_wandb and is_main_process()
|
||
if use_wandb:
|
||
wandb = args.wandb
|
||
wandb_id = ckp_data.get("wandb_id") if ckp_data else None
|
||
resume = "must" if wandb_id else None
|
||
wandb_run_name = f"MiniMind-{round(num_params/1e6)}M-{args.save_weight}-epoch-{args.epochs}-batchsize-{args.batch_size}-lr-{args.learning_rate}"
|
||
if wandb_id:
|
||
config = None
|
||
else:
|
||
config = asdict(args)
|
||
for key in [
|
||
"device",
|
||
"from_resume",
|
||
"use_wandb",
|
||
"wandb_project",
|
||
"save_dir",
|
||
]:
|
||
config.pop(key)
|
||
config["max_step"] = max_step
|
||
config["num_params"] = num_params
|
||
wandb_kwargs = dict(
|
||
entity=args.wandb_entity,
|
||
project=args.wandb_project,
|
||
name=wandb_run_name,
|
||
id=wandb_id,
|
||
resume=resume,
|
||
config=config,
|
||
)
|
||
if args.use_swanlab:
|
||
wandb_kwargs.pop("entity")
|
||
run = wandb.init(**wandb_kwargs)
|
||
if args.use_swanlab:
|
||
run = nullcontext()
|
||
else:
|
||
wandb.define_metric("train_step")
|
||
wandb.define_metric("eval_step")
|
||
wandb.define_metric("train/*", step_metric="train_step")
|
||
wandb.define_metric("eval/*", step_metric="eval_step")
|
||
|
||
# ========== 8. 开始训练 ==========
|
||
bar = tqdm(total=max_step, disable=not is_main_process())
|
||
with run:
|
||
for epoch in range(start_epoch, args.epochs):
|
||
train_sampler and train_sampler.set_epoch(epoch)
|
||
if epoch == start_epoch and start_step > 0: # 第一个epoch且存在检查点
|
||
Logger(
|
||
f"Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始"
|
||
)
|
||
# train_epoch(epoch, loader, len(loader) + start_step + 1, start_step, wandb)
|
||
_iters = iters + start_step + 1
|
||
bar.update(start_step)
|
||
else: # 默认从头开始
|
||
_iters = iters
|
||
spend_time = train_epoch(
|
||
model=model,
|
||
args=args,
|
||
lm_config=lm_config,
|
||
optimizer=optimizer,
|
||
scheduler=scheduler,
|
||
scaler=scaler,
|
||
epoch=epoch,
|
||
loader=loader,
|
||
iters=_iters,
|
||
start_step=start_step,
|
||
bar=bar,
|
||
use_wandb=use_wandb,
|
||
spend_time=spend_time,
|
||
)
|
||
bar.close()
|
||
|
||
|
||
# ----- RAG -----
|
||
|
||
|
||
@dataclass
|
||
class EvalArgs:
|
||
load_from: str = project_dir / "model"
|
||
save_dir: str = "out"
|
||
weight: str = "full_sft"
|
||
lora_weight: str = "None"
|
||
hidden_size: int = 768 # chat模型隐藏层维度(与retriever无关)
|
||
num_hidden_layers: int = 16 # chat模型隐藏层数量(与retriever无关)
|
||
use_moe: int = 0
|
||
inference_rope_scaling: bool = False
|
||
max_new_tokens: int = 8192
|
||
temperature: float = 0.85
|
||
top_p: float = 0.85
|
||
top_k: int = 20 # 检索时返回的文档数量,reranker会从中选择最终答案
|
||
rag: bool = True # 是否开启RAG
|
||
max_query_len: int = 64
|
||
max_passage_len: int = 128
|
||
historys: int = 0
|
||
use_sbert_retriever: bool = True # 使用sentence-transformer作为retriever,检索成功率会大幅提升
|
||
device: str = None
|
||
|
||
def __post_init__(self):
|
||
if self.device is None:
|
||
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
||
self.save_dir = Path(self.save_dir)
|
||
if not self.save_dir.is_absolute():
|
||
self.save_dir = project_dir / self.save_dir
|
||
if not self.save_dir.exists():
|
||
self.save_dir = project_dir / "checkpoints"
|
||
self.save_dir = self.save_dir.as_posix()
|
||
if isinstance(self.load_from, Path):
|
||
self.load_from = self.load_from.as_posix()
|
||
|
||
|
||
@torch.inference_mode()
|
||
def init_rag(
|
||
device,
|
||
documents: Union[List[str], datasets.Dataset] = None,
|
||
retriever: Encoder = None,
|
||
tokenizer: PreTrainedTokenizerBase = None,
|
||
eval_args: EvalArgs = None,
|
||
):
|
||
print("loading models and encoding documents, this may take a while...")
|
||
if retriever is None or eval_args.use_sbert_retriever:
|
||
retriever = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
|
||
encode_fn = lambda retriever: retriever.encode(
|
||
documents, convert_to_tensor=True, show_progress_bar=True
|
||
)
|
||
else:
|
||
|
||
def encode_fn(retriever: Encoder):
|
||
all_p_embeddings = []
|
||
batch_size = 128
|
||
step = int(np.ceil(len(documents) / batch_size))
|
||
for i in trange(step):
|
||
docs = documents[i * batch_size : (i + 1) * batch_size]
|
||
p_embeddings = retriever.encode_text(
|
||
docs, tokenizer, max_length=eval_args.max_passage_len
|
||
)
|
||
all_p_embeddings.append(p_embeddings)
|
||
p_embeddings = torch.cat(
|
||
all_p_embeddings, dim=0
|
||
) # (n_passages, hidden_dim)
|
||
return p_embeddings
|
||
|
||
retriever.to(device)
|
||
retriever.eval()
|
||
reranker = AutoModelForSequenceClassification.from_pretrained(
|
||
"jinaai/jina-reranker-v2-base-multilingual", trust_remote_code=True
|
||
)
|
||
reranker.to(device)
|
||
reranker.eval()
|
||
# return retriever, reranker
|
||
if documents is None:
|
||
documents = load_dataset()
|
||
documents = documents["train"]["context"]
|
||
doc_embeddings = encode_fn(retriever)
|
||
return retriever, reranker, doc_embeddings
|
||
|
||
|
||
@torch.inference_mode()
|
||
def retrieve(
|
||
query: str,
|
||
retriever: Union[SentenceTransformer, Encoder],
|
||
reranker: PreTrainedModel,
|
||
documents: List[str],
|
||
doc_embeddings: torch.Tensor,
|
||
top_k=20,
|
||
tokenizer=None,
|
||
max_length=128,
|
||
):
|
||
"""
|
||
corpus_embeddings: (n_passages, hidden_dim)
|
||
"""
|
||
if isinstance(retriever, SentenceTransformer):
|
||
query_embeddings = retriever.encode(
|
||
query, convert_to_tensor=True
|
||
) # (hidden_dim,)
|
||
else:
|
||
query_embeddings = retriever.encode_text(
|
||
query, tokenizer, max_length=max_length
|
||
)[0]
|
||
similarity = doc_embeddings @ query_embeddings
|
||
values, indices = torch.topk(similarity, k=top_k)
|
||
indices = indices.cpu().numpy()
|
||
sentence_pairs = [[query, documents[i]] for i in indices]
|
||
scores = reranker.compute_score(sentence_pairs)
|
||
max_index = indices[np.argsort(scores)[-1]]
|
||
return max_index.item(), documents[max_index]
|
||
|
||
|
||
@torch.inference_mode()
|
||
def evaluate(
|
||
args: EvalArgs,
|
||
retriever: SentenceTransformer,
|
||
reranker: PreTrainedModel,
|
||
documents: List[str],
|
||
doc_embeddings: torch.Tensor,
|
||
):
|
||
prompts = [
|
||
"估计有4.88亿至5.35亿人信奉什么宗教",
|
||
"南安普敦机场在哪个城镇",
|
||
"路德什么时候死的",
|
||
"超级碗开幕之夜于何时在何地举行",
|
||
]
|
||
|
||
conversation = []
|
||
model, tokenizer = init_model(eval_args=args)
|
||
input_mode = int(input("[0] 自动测试\n[1] 手动输入\n"))
|
||
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
||
|
||
prompt_iter = prompts if input_mode == 0 else iter(lambda: input("👶: "), "")
|
||
for prompt in prompt_iter:
|
||
setup_seed(2026) # or setup_seed(random.randint(0, 2048))
|
||
if input_mode == 0:
|
||
print(f"👶: {prompt}")
|
||
if args.rag:
|
||
doc = retrieve(
|
||
prompt,
|
||
retriever,
|
||
reranker,
|
||
documents,
|
||
doc_embeddings,
|
||
top_k=args.top_k,
|
||
tokenizer=tokenizer,
|
||
max_length=args.max_passage_len,
|
||
)[1]
|
||
prompt = f'你是一个智能问答助手,请严格按照以下要求回答问题: 如果资料中包含问题答案,请直接使用资料信息并注明"根据资料",如果资料不相关、信息不足或未包含答案,请明确说明"资料中未包含相关信息",然后可以基于常识进行补充\n资料:{doc}\n问题:{prompt}\n现在请开始回答:'
|
||
conversation = conversation[-args.historys :] if args.historys else []
|
||
conversation.append({"role": "user", "content": prompt})
|
||
|
||
templates = {
|
||
"conversation": conversation,
|
||
"tokenize": False,
|
||
"add_generation_prompt": True,
|
||
}
|
||
if args.weight == "reason":
|
||
templates["enable_thinking"] = True # 仅Reason模型使用
|
||
inputs = (
|
||
tokenizer.apply_chat_template(**templates)
|
||
if args.weight != "pretrain"
|
||
else (tokenizer.bos_token + prompt)
|
||
)
|
||
inputs = tokenizer(inputs, return_tensors="pt", truncation=True).to(args.device)
|
||
|
||
print("🤖️:", end="")
|
||
generated_ids = model.generate(
|
||
inputs=inputs["input_ids"],
|
||
attention_mask=inputs["attention_mask"],
|
||
max_new_tokens=args.max_new_tokens,
|
||
do_sample=True,
|
||
streamer=streamer,
|
||
pad_token_id=tokenizer.pad_token_id,
|
||
eos_token_id=tokenizer.eos_token_id,
|
||
top_p=args.top_p,
|
||
temperature=args.temperature,
|
||
repetition_penalty=1.0,
|
||
)
|
||
response = tokenizer.decode(
|
||
generated_ids[0][len(inputs["input_ids"][0]) :], skip_special_tokens=True
|
||
)
|
||
conversation.append({"role": "assistant", "content": response})
|
||
print("")
|