比赛背景

该比赛算是“书生大模型实战营-第五期”的沐曦魔乐专场。

比赛任务就是在沐曦算力下微调InternLM-1.5B进行论文分类,由沐曦和魔乐社区提供赞助和平台支持,要求呢就是必须得用沐曦的算力来微调。

笔者最终比赛成绩为83.36,在比赛结束后特将比赛方案分享于此。

创建沐曦算力容器及配置环境

本任务使用D.run平台提供的沐曦算力完成,大部分时间白嫖了32G的,高峰期则使用了香港一区的64G。

https://console.d.run/zestu/market?regionId=sh-06

选择Pytorch镜像

安装 ms-swift

1
2
3
conda create -n ms-swift python=3.10 -y
conda activate ms-swift
pip install ms-swift -U

中途服务器镜像更新过一次,最新版的镜像可能需要先 pip uninstall flash_attn 再执行以下命令,不然会有版本问题。

(PS:本文具有时效性,不确保该方案会适用于D.run未来更新后的镜像)

然后安装mx编译后的包

创建requirements.txt内容如下

1
2
3
4
5
6
apex==0.1+metax2.32.0.3
torch==2.6.0+metax2.32.0.3
torchaudio==2.4.1+metax2.32.0.3
torchvision==0.15.1+metax2.32.0.3
triton==3.0.0+metax2.32.0.3
flash_attn
1
pip install -r requirements.txt -i https://repos.metax-tech.com/r/maca-pypi/simple --trusted-host repos.metax-tech.com --no-build-isolation

安装提交魔乐平台的包

1
2
pip install openmind_hub[all]
pip install "openmind[pt]" --extra-index-url https://download.pytorch.org/whl/cpu

构建数据集

考虑到公开的示例数据是十分类,并且只包括了26类中的6类,直接使用示例数据来进行训练上限肯定是有限的。(最早我试了,应该只能到四十几分数)

所以需要自己根据arXiv数据集造数据,我是直接把arXiv数据集下载到本地进行处理的。

先对数据集做一个简单的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import json
from collections import Counter
from pathlib import Path

# 原始类别映射(英文 → A-Z)
CATEGORY_MAP = {
"quant-ph": "A", "physics.chem-ph": "B", "physics.atom-ph": "C",
"cond-mat.soft": "D", "cs.RO": "E", "cs.CL": "F", "cs.SE": "G",
"cs.IR": "H", "hep-th": "I", "hep-ph": "J", "physics.optics": "K",
"cs.AI": "L", "cs.CV": "M", "nucl-th": "N", "astro-ph": "O",
"math.PR": "P", "cs.OS": "Q", "eess.SP": "R", "math.OC": "S",
"math.DS": "T", "math.DG": "U", "math.MP": "V", "cs.MM": "W",
"stat.ME": "X", "math.CO": "Y", "cs.NE": "Z"
}

# 中文名映射(A-Z → 中文)
CATEGORY_NAME_MAP = {
"A": "量子物理", "B": "化学物理", "C": "原子物理", "D": "软凝聚态物理", "E": "机器人学",
"F": "计算语言学", "G": "软件工程", "H": "信息检索", "I": "高能理论物理", "J": "高能现象学",
"K": "光学", "L": "人工智能", "M": "计算机视觉", "N": "核理论", "O": "天体物理",
"P": "概率论", "Q": "操作系统", "R": "信号处理", "S": "最优化与控制", "T": "动力系统",
"U": "微分几何", "V": "数学物理", "W": "多媒体", "X": "统计方法", "Y": "组合数学", "Z": "神经与进化计算"
}


def analyze_arxiv_categories(jsonl_path: str):
single_label_counter = Counter()
multi_label_counter = Counter()
total_single = 0
total_multi = 0

with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
try:
paper = json.loads(line)
raw_cat_str = paper.get("categories", "")
raw_cats = raw_cat_str.strip().split()
mapped_cats = [CATEGORY_MAP[cat] for cat in raw_cats if cat in CATEGORY_MAP]
unique_cats = list(set(mapped_cats))

if not unique_cats:
continue

if len(unique_cats) == 1:
single_label_counter[unique_cats[0]] += 1
total_single += 1
else:
for c in unique_cats:
multi_label_counter[c] += 1
total_multi += 1
except json.JSONDecodeError:
print("⚠️ 无法解析以下行:", line.strip()[:80])

def display(counter, total, title):
print(f"\n===== {title} =====")
print(f"总论文数:{total}")
for cat in sorted(CATEGORY_NAME_MAP.keys()):
count = counter[cat]
percent = (count / total * 100) if total > 0 else 0
name = CATEGORY_NAME_MAP[cat]
print(f"类别 {cat}{name}): {count} 篇,占比 {percent:.2f}%")

display(single_label_counter, total_single, "单标签论文统计")
display(multi_label_counter, total_multi, "多标签论文统计")


if __name__ == "__main__":
jsonl_file = "arxiv_raw.json" # 修改为你的真实路径
if not Path(jsonl_file).exists():
print(f"❌ 找不到文件:{jsonl_file}")
else:
analyze_arxiv_categories(jsonl_file)

分析结果

原本数据是不平衡的,并且有多类别的干扰,比较棘手。最开始我有尝试把多类别数据加进来,但去除同时包含这26类中的多类的,但后续感觉还不如直接用单类别的论文来的效果好。

抽取26类的子集

以下是从arXiv原始数据集中选择子集的脚本

这里放的是我最终采用的方法:直接舍弃了多类别数据,只看单一类别的论文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import argparse
import json
import random
from pathlib import Path
from collections import defaultdict

# 目标 26 类
TARGET_CATEGORIES = [
"quant-ph", "physics.chem-ph", "physics.atom-ph", "cond-mat.soft",
"cs.RO", "cs.CL", "cs.SE", "cs.IR", "hep-th", "hep-ph",
"physics.optics", "cs.AI", "cs.CV", "nucl-th", "astro-ph",
"math.PR", "cs.OS", "eess.SP", "math.OC", "math.DS",
"math.DG", "math.MP", "cs.MM", "stat.ME", "math.CO", "cs.NE"
]

# —— 在这里定义 custom 模式下的各类抽样数 ——
# 指定 6 类为 10,其余 20 类为 60
CUSTOM_COUNTS = {
# 6 类,每类数量 10
"astro-ph": 0,
"cs.CL": 0,
"cs.CV": 0,
"hep-ph": 0,
"hep-th": 0,
"quant-ph": 0,
# 其余 20 类,每类数量 60
"physics.chem-ph": 60,
"physics.atom-ph": 60,
"cond-mat.soft": 60,
"cs.RO": 60,
"cs.SE": 60,
"cs.IR": 60,
"physics.optics": 60,
"cs.AI": 60,
"nucl-th": 60,
"math.PR": 60,
"cs.OS": 60,
"eess.SP": 60,
"math.OC": 60,
"math.DS": 60,
"math.DG": 60,
"math.MP": 60,
"cs.MM": 60,
"stat.ME": 60,
"math.CO": 60,
"cs.NE": 60,
}
# ————————————————————————————————

def sample_and_shuffle(input_path: Path,
output_path: Path,
mode: str,
uniform_count: int,
seed: int):
random.seed(seed)
buckets = defaultdict(list)

# 1) 收集恰好单标签记录
with input_path.open('r', encoding='utf-8') as fin:
for line in fin:
rec = json.loads(line)
# 拆分 raw categories
raw = []
for seg in rec.get("categories", "").split():
raw += seg.split(',')
cats = [c.strip() for c in raw if c.strip()]
matched = [c for c in cats if c in TARGET_CATEGORIES]
if len(matched) == 1:
cat = matched[0]
rec["categories"] = cat
buckets[cat].append(rec)

# 2) 按模式确定每类抽样数并采样
sampled = []
for cat in TARGET_CATEGORIES:
recs = buckets.get(cat, [])
total = len(recs)
if mode == "uniform":
n = min(total, uniform_count)
else: # custom
if cat not in CUSTOM_COUNTS:
raise ValueError(f"Custom counts 未包含类别 '{cat}'")
n = min(total, CUSTOM_COUNTS[cat])
chosen = random.sample(recs, n)
sampled.extend(chosen)
print(f"{cat}: 总数 {total},抽取 {n} 条")

# 3) 打乱全局并写入
random.shuffle(sampled)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open('w', encoding='utf-8') as fout:
for rec in sampled:
fout.write(json.dumps(rec, ensure_ascii=False) + "\n")

print(f"\n✅ 完成!模式:{mode},结果保存在 {output_path}")

if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="按 26 类单标签抽样并打乱,支持统一或自定义数量两种模式"
)
parser.add_argument("-i","--input", type=Path, default="arxiv_raw.json",
help="原始 Arxiv JSONL 文件路径")
parser.add_argument("-o","--output", type=Path, default="arxiv_26_subset.jsonl",
help="输出子集 JSONL 文件路径")
parser.add_argument("--mode", choices=["uniform","custom"], default="uniform",
help="抽样模式:uniform(统一数量)或 custom(使用脚本中定义的字典)")
parser.add_argument("-n","--count", type=int, default=1000,
help="若 mode=uniform,每类抽取的数量")
parser.add_argument("-s","--seed", type=int, default=24,
help="随机种子,保证可复现")
args = parser.parse_args()

sample_and_shuffle(
input_path=args.input,
output_path=args.output,
mode=args.mode,
uniform_count=args.count,
seed=args.seed
)

使用了两种模式,custom为自定义每一类的数量,uniform为统一数量生成。主要原因是最开始我发现,部分数据使用官方公开的示例数据效果似乎可以有一点点提升,所以为了数据类别的平衡性就选择自定义数量生成之后和示例数据做一个合并。

但后面随着训练数据量的变大就懒得采用这种方法了,直接选择了统一数量随机生成。

构建预训练与sft数据

先有抽取脚本抽取出jsonl格式的子集,然后作为输入文件运行以下脚本

创建一个make_pretrain.py脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import json
from pathlib import Path
from dateutil import parser as dateparser # pip install python-dateutil

# 支持的 26 类
SUPPORTED_CATEGORIES = [
"quant-ph","physics.chem-ph","physics.atom-ph","cond-mat.soft",
"cs.RO","cs.CL","cs.SE","cs.IR","hep-th","hep-ph",
"physics.optics","cs.AI","cs.CV","nucl-th","astro-ph",
"math.PR","cs.OS","eess.SP","math.OC","math.DS",
"math.DG","math.MP","cs.MM","stat.ME","math.CO","cs.NE"
]

def format_versions(versions):
latest = versions[-1]
v = latest.get("version", "v?")
created = latest.get("created", "")
try:
dt = dateparser.parse(created)
created_fmt = dt.strftime("%B %-d, %Y")
except Exception:
created_fmt = created
return v, created_fmt

def build_pretrain_record(rec: dict) -> dict:
pid = rec.get("id", "Unknown")
title = rec.get("title", "").strip()
submitter = rec.get("submitter", "Unknown")
authors = rec.get("authors", "").strip()
journal = rec.get("journal-ref") or ""
doi = rec.get("doi") or ""
license_ = rec.get("license") or ""
abstract = rec.get("abstract", "").strip()

# 只保留第一个匹配的支持类别
raw = rec.get("categories", "")
parts = []
for seg in raw.split():
parts += seg.split(',')
parts = [c.strip() for c in parts if c.strip()]
selected = next((c for c in parts if c in SUPPORTED_CATEGORIES), None)
if not selected:
return None # 不匹配则跳过
categories = selected

# 版本信息
if rec.get("versions"):
v, vdate = format_versions(rec["versions"])
else:
v, vdate = "v?", "Unknown date"

# 构造 content
content = (
f"This is a paper with ID {pid}, titled \"{title}\", submitted by {submitter}. "
f"The authors are {authors}.\n"
)
if journal:
content += f"It is published in {journal}. "
else:
content += "It is not published in any journal. "
content += f"The latest version is {v}, created on {vdate}. "
if doi:
content += f"DOI: {doi}. "
else:
content += "No DOI information available. "
if license_:
content += f"The license is {license_}. "
else:
content += "No license information available. "
content += f"\n\nAbstract:\n{abstract}\n\n"
content += f"The paper belongs to the {categories} category."

return {"messages":[{"role":"assistant","content": content}]}

def main():
parser = argparse.ArgumentParser(
description="将 Arxiv JSONL 构造成 Pretrain 格式,仅保留支持的 26 类"
)
parser.add_argument("-i","--input", type=Path, required=True, help="Arxiv 原始 JSONL")
parser.add_argument("-o","--output", type=Path, required=True, help="输出 Pretrain JSONL")
args = parser.parse_args()

args.output.parent.mkdir(parents=True, exist_ok=True)
kept = 0
with args.input.open('r', encoding='utf-8') as fin, \
args.output.open('w', encoding='utf-8') as fout:
for line in fin:
rec = json.loads(line)
pre = build_pretrain_record(rec)
if pre:
fout.write(json.dumps(pre, ensure_ascii=False) + "\n")
kept += 1

print(f"✅ 完成!共保留 {kept} 条记录,输出到 {args.output}")

if __name__=="__main__":
main()

make_sft.py脚本如下(修改提示词直接在此脚本进行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import json
from pathlib import Path

# 支持的 26 类(A–Z 顺序)
LABELS = [
"quant-ph", "physics.chem-ph", "physics.atom-ph", "cond-mat.soft",
"cs.RO", "cs.CL", "cs.SE", "cs.IR", "hep-th", "hep-ph",
"physics.optics", "cs.AI", "cs.CV", "nucl-th", "astro-ph",
"math.PR", "cs.OS", "eess.SP", "math.OC", "math.DS",
"math.DG", "math.MP", "cs.MM", "stat.ME", "math.CO", "cs.NE"
]

SYSTEM_PROMPT = "你是个优秀的论文分类师"

SUFFIX = (
"please determine the scientific category of this paper. \n\n"
"A. quant-ph\n"
"B. physics.chem-ph\n"
"C. physics.atom-ph\n"
"D. cond-mat.soft\n"
"E. cs.RO\n"
"F. cs.CL\n"
"G. cs.SE\n"
"H. cs.IR\n"
"I. hep-th\n"
"J. hep-ph\n"
"K. physics.optics\n"
"L. cs.AI\n"
"M. cs.CV\n"
"N. nucl-th\n"
"O. astro-ph\n"
"P. math.PR\n"
"Q. cs.OS\n"
"R. eess.SP\n"
"S. math.OC\n"
"T. math.DS\n"
"U. math.DG\n"
"V. math.MP\n"
"W. cs.MM\n"
"X. stat.ME\n"
"Y. math.CO\n"
"Z. cs.NE"
)

def build_sft_record(rec: dict):
"""
构造 SFT 记录,若无匹配类别则返回 None。
"""
title = rec.get("title", "").strip()
authors = rec.get("authors", "").strip()
abstract = rec.get("abstract", "").strip()
cats_raw = rec.get("categories", "")

# 拆分多类别字段
parts = []
for seg in cats_raw.split():
parts += seg.split(',')
parts = [c.strip() for c in parts if c.strip()]

# 选第一个出现在 LABELS 中的类别
selected = next((c for c in parts if c in LABELS), None)
if selected is None:
return None, parts # 返回 parts 供日志

idx = LABELS.index(selected)
answer = chr(ord('A') + idx)

human = (
f"Based on the title '{title}', authors '{authors}', and abstract '{abstract}',\n"
+ SUFFIX
)

return {
"system": SYSTEM_PROMPT,
"conversation": [
{"human": human, "assistant": answer}
]
}, None

def main():
parser = argparse.ArgumentParser(
description="将多类别 JSONL 构造成 26 类 SFT 数据,只取首个匹配类别;并输出无匹配项"
)
parser.add_argument("-i", "--input", type=Path, required=True,
help="子集 JSONL 输入路径")
parser.add_argument("-o", "--output", type=Path, required=True,
help="输出 SFT JSONL 路径")
args = parser.parse_args()

args.output.parent.mkdir(parents=True, exist_ok=True)
kept = 0
skipped = []

with args.input.open('r', encoding='utf-8') as fin, \
args.output.open('w', encoding='utf-8') as fout:
for ln, line in enumerate(fin, 1):
rec = json.loads(line)
sft_rec, unmatched_parts = build_sft_record(rec)
if sft_rec:
fout.write(json.dumps(sft_rec, ensure_ascii=False) + "\n")
kept += 1
else:
# 记录行号与原始类别列表
skipped.append((ln, unmatched_parts))

print(f"✅ 完成:保留 {kept} 条记录")
if skipped:
print(f"❌ 共 {len(skipped)} 条无匹配类别,详情:")
for ln, parts in skipped:
print(f" 行 {ln}: 原类别 {parts}")

if __name__ == "__main__":
main()

构建验证集(给OpenCompass的评测数据)

使用以下脚本根据原始数据集提取出的子集jsonl生成一个可用于OpenCompass单选题评测的csv。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import json
import csv

# 输入/输出文件
INPUT_FILE = 'arxiv_26_subset.jsonl'
OUTPUT_FILE = 'val_dataset2.csv'

# A~Z 对应的类别顺序
letters = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
categories = [
"quant-ph",
"physics.chem-ph",
"physics.atom-ph",
"cond-mat.soft",
"cs.RO",
"cs.CL",
"cs.SE",
"cs.IR",
"hep-th",
"hep-ph",
"physics.optics",
"cs.AI",
"cs.CV",
"nucl-th",
"astro-ph",
"math.PR",
"cs.OS",
"eess.SP",
"math.OC",
"math.DS",
"math.DG",
"math.MP",
"cs.MM",
"stat.ME",
"math.CO",
"cs.NE"
]

# 将类别映射到字母
cat2letter = {cat: let for let, cat in zip(letters, categories)}

with open(INPUT_FILE, 'r', encoding='utf-8') as fin, \
open(OUTPUT_FILE, 'w', encoding='utf-8', newline='') as fout:

writer = csv.writer(fout)
# 写 header
writer.writerow(['question'] + letters + ['answer'])

for line in fin:
rec = json.loads(line)

title = (rec.get('title') or '').replace('\n', ' ').strip()
authors = (rec.get('authors') or '').replace('\n', ' ').strip()
abstract = (rec.get('abstract') or '').replace('\n', ' ').strip()
comments = (rec.get('comments') or '').replace('\n', ' ').strip()

extra = f" Additional info: {comments}" if comments else ""

question = (
f"Based on the title '{title}', authors '{authors}', "
f"and abstract '{abstract}', please determine the scientific category of this paper."
+ extra
)

primary_cat = (rec.get('categories') or '').split(',')[0].strip()
answer = cat2letter.get(primary_cat, '')

if not answer:
print(f"警告:未识别类别 '{primary_cat}',跳过这条记录")
continue

row = [question] + categories + [answer]
writer.writerow(row)

print(f"转换完成,已生成:{OUTPUT_FILE}")

在编写一个配置脚本,设置好模型路径、数据集路径和训练参数,用OpenCompass测评即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from mmengine.config import read_base
from opencompass.models import TurboMindModelwithChatTemplate
from opencompass.models import HuggingFaceCausalLM
models = [
dict(
type=TurboMindModelwithChatTemplate,
abbr=f'internlm2-papercls',
# model path, which can be the address of a model repository on the Hugging Face Hub or a local path
path='/root/swift_output/InternLM2-sft/v35-20250805-194627/checkpoint-1300-merged',
backend='turbomind',
engine_config=dict(tp=1),
gen_config=dict(do_sample=False),
max_seq_len=4096,
max_out_len=100,
batch_size=5000,
run_cfg=dict(num_gpus=1),
)
]

datasets = [
# {"path": "/root/arXiv/val_dataset1.csv", "data_type": "mcq", "infer_method": "gen"},
# {"path": "/root/arXiv/val_dataset2.csv", "data_type": "mcq", "infer_method": "gen"},
{"path": "/root/arXiv/val_dataset3.csv", "data_type": "mcq", "infer_method": "gen"},
# {"path": "/root/arXiv/val_dataset4.csv", "data_type": "mcq", "infer_method": "gen"},
]

work_dir = '/root/val'

高峰期的B榜测评往往要数小时,所以本地有个评测的验证集还是挺有用的。(我大概用了2w+的数据构建了一个验证集)

很巧的是在我自己的验证集上表现最好的也是我最终有效最高分对应的模型,跑了86.12。

预训练

参考baseline文档来编写的训练脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/bin/bash

# 创建日志目录
LOG_DIR="logs"
mkdir -p $LOG_DIR # 确保日志目录存在,如果不存在则创建

# 获取当前时间戳,用于生成唯一的日志文件名
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOG_FILE="$LOG_DIR/pretrain/internlm2_lora_${TIMESTAMP}.log" # 设置日志文件路径

# 设置CUDA环境变量
export NPROC_PER_NODE=1 # 设置每个节点使用的进程数为1
export OMP_NUM_THREADS=1 # 限制OpenMP线程数为1,避免过多线程竞争
export CUDA_VISIBLE_DEVICES=0 # 指定使用的GPU编号为0
export MASTER_PORT=$((10000 + RANDOM % 50000))

# 使用nohup命令在后台运行训练任务,即使终端关闭也能继续运行
nohup swift sft \
--model /root/public-model/models/Shanghai_AI_Laboratory/internlm2_5-1_8b-chat \
--train_type lora \
--dataset /root/arXiv/pretrain_26.jsonl \
--torch_dtype float16 \
--num_train_epochs 10 \
--per_device_train_batch_size 16 \
--learning_rate 5e-5 \
--warmup_ratio 0.1 \
--split_dataset_ratio 0 \
--lora_rank 16 \
--lora_alpha 32 \
--use_chat_template false \
--target_modules all-linear \
--gradient_accumulation_steps 2 \
--save_steps 2000 \
--save_total_limit 3 \
--gradient_checkpointing_kwargs '{"use_reentrant": false}' \
--logging_steps 5 \
--max_length 2048 \
--output_dir ./swift_output/InternLM2-pretrain \
--dataloader_num_workers 128 \
--model_author octal \
--model_name InternLM2-papercls \
> "$LOG_FILE" 2>&1 &

# 打印进程ID和日志文件位置,便于用户跟踪
echo "Training started with PID $!" # 显示后台进程的PID
echo "Log file: $LOG_FILE" # 显示日志文件位置

# 提示用户如何实时查看日志
echo "To view logs in real-time, use:"
echo "tail -f $LOG_FILE"

训练完 swift export 合并一下权重就ok了。

1
swfit export --adpters /path/to/your/model/ --merge_lora true

SFT 指令微调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/bash
# 指定使用bash解释器执行脚本

# 创建日志目录
LOG_DIR="logs"
# 定义日志存储目录变量
mkdir -p $LOG_DIR
# 创建日志目录,-p参数确保即使目录已存在也不会报错

# 获取当前时间戳
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# 获取当前时间并格式化为年月日_时分秒格式
LOG_FILE="$LOG_DIR/sft/internlm2_lora_${TIMESTAMP}.log"
# 组合日志文件路径,使用时间戳确保文件名唯一

# 设置CUDA设备
# 设置每个节点的进程数为1
export OMP_NUM_THREADS=1
# 设置OpenMP线程数为1,限制并行线程数
export CUDA_VISIBLE_DEVICES=0
# 指定使用的GPU设备为0号设备

nohup swift sft \
--model /root/public-model/models/Shanghai_AI_Laboratory/internlm2_5-1_8b-chat \
--train_type lora \
--dataset '/root/arXiv/sft_26.jsonl' \
--torch_dtype float16 \
--num_train_epochs 4 \
--per_device_train_batch_size 8 \
--learning_rate 1e-4 \
--warmup_ratio 0.1 \
--split_dataset_ratio 0 \
--lora_rank 16 \
--lora_alpha 32 \
--target_modules all-linear \
--gradient_accumulation_steps 2 \
--save_steps 2000 \
--save_total_limit 5 \
--gradient_checkpointing_kwargs '{"use_reentrant": false}' \
--logging_steps 5 \
--max_length 2048 \
--output_dir ./swift_output/InternLM2-sft \
--dataloader_num_workers 128 \
--model_author octal \
--model_name InternLM2-sft-octal \
> "$LOG_FILE" 2>&1 &
# 将标准输出和标准错误重定向到日志文件,并在后台运行

# 打印进程ID和日志文件位置
echo "Training started with PID $!"
# 显示训练进程的PID($!代表最近一个后台进程的PID)
echo "Log file: $LOG_FILE"
# 显示日志文件的路径

# 显示查看日志的命令
echo "To view logs in real-time, use:"
echo "tail -f $LOG_FILE"
# 提示用户如何实时查看日志文件内容

训练了多个模型后,总结出的经验:对于1.8B模型训练不同规模的数据,第一轮sft训练基本都是2~4个epoch为最佳,再继续训练的准确率反而更可能下降。如果此前pretrain的轮次多,sft的轮次似乎也可以略高一点,但也不能过高。(在隔壁赛道试验发现,8B模型的最佳训练轮次似乎是要更多一些)

最开始我是使用了1k左右的数据复现以上的baseline,其实评测结果已经达到了60分左右。

我又扩大数据量到3k左右(平均采样),其实就妥妥达到70分了。所以我觉得只要跟着教程耐心复现,拿个三等奖是难度不大的。

baseline基础上继续优化的思路

由于隔壁10分类赛道可以使用A100算力,更便于我不断去训练尝试。所以我是先把10分类的赛道分数刷到还算看得过去再来做本赛道的。而对10分类来说,我当时测出来似乎并非数据量越大越好,所以一开始就没想着扩大数据量。

所以在开始做26类的任务时,我最初是把数据量的数量级稳定在3k,然后从优化数据质量入手。

过程中尝试了许多方法,这里先列举几个试验过程(但非产生最优模型的主要因素):

  1. 重写sft的system prompt:例如把中文改成英文,运用提示词工程的常见手段丰富内容等。最终效果却都不理想,改成英文后甚至会有明显的成绩下滑。得出的结论是:还是用一句简洁明练的中文提示词最为有效。
  2. 在平衡类别的基础上再平衡发表年份,以采集更多样化的论文风格:也提升极小,聊胜于无,后续就没这么折腾了。
  3. 使用更多的提问模版(直接让GPT写):新增了4个模板,增加数据的多样性。喜报是成绩有明显提升了,不过现在回想起来也可能是数据量变大所带来的长进。
  4. ReSFT:训完一个模型后用OpenCompass在验证集做一次测评,再用回答错的论文信息对模型进行2次训练。此方法刷榜时还是挺有用的,当模型达到一个还不错的分数且和前面选手相差不大的时候,这样多次训练一下往往还是可以让成绩再提0.0x甚至0.x的。

前2/3以上的赛程中,其实我都在用3k左右的数据量+各种优化尝试训了一些模型结果。但此时的最好结果就只有80点几,大部分提交都在75~80分。

到后期大家开始冲榜,卷起来的时候,这个分已经不够看了,以每天十几名的速度不断往下掉。

后面病急乱投医,我用1w+的数据试了一下,结果突然就有了明显优化。本地OpenCompass评测验证集的结果更是一下子提了5个点。

最后又用2w+的数据跑了一次(当时觉得已经够多了,毕竟OS类就一千篇左右,再多好像就没法让类别分布平衡了),跑完用上述说的“ReSFT”又优化了零点几,就成了最终83.36的结果。

后续由于时间原因也没有再试更多数据了,压线拿了个60名还是非常幸运的!😁

感悟:在微调数据质量有保障的情况下,扩大数据量终归是硬道理😂