Files
exp-model-parameter-similarity/finetune_compare/report.py
T

74 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Paired concentration changes from saved immutable intervention arrays."""
import csv,json,sys
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from dominance.core import concentration
def stats(x):
x=np.asarray(x,dtype=np.float64);v=x[np.isfinite(x)];q=np.quantile(v,[.1,.5,.9])
return {'count':int(v.size),'p10':float(q[0]),'median':float(q[1]),'p90':float(q[2]),'mean':float(v.mean())}
def csvwrite(path,rows):
with path.open('w') as f:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
def main(root):
root=Path(root);status=json.loads((root/'status.json').read_text());assert status['state']=='completed';samples=json.loads((root/'samples.json').read_text());pool={};observations={};summary={};rows=[];layer_rows=[]
for cohort in ('legacy','gsm8k'):
for role in ('base','finetuned'):
for sample in samples[cohort]:
for source in (2,6,10,14,18,22):
data=np.load(root/'raw'/f'{cohort}-{role}-{sample["id"]}-{source:02d}.npz')
for metric,key,transform in [('distance','JS',np.sqrt),('divergence','JS',lambda x:x),('QKV','QKV',lambda x:x)]:
impacts=transform(data['single_'+key])[:,7:,:].transpose(1,2,0);c=concentration(impacts)
record={'cohort':cohort,'role':role,'sample':sample['id'],'recipient':source+1,'metric':metric}
for name,x in c.items():
observations.setdefault((cohort,role,metric,name),[]).append(x.reshape(-1))
if name!='valid':
for stat,value in stats(x).items():record[name+'_'+stat]=value
pool.setdefault((cohort,role,metric,'recipient'),[]).append(np.full(200,source+1))
pool.setdefault((cohort,role,metric,'sample'),[]).append(np.full(200,sample['id']))
observations.setdefault((cohort,role,metric,'winner'),[]).append(np.argmax(impacts,-1).reshape(-1))
rows.append(record)
arrays={k:np.concatenate(v) for k,v in observations.items()};np.savez_compressed(root/'observations.npz',**{'__'.join(k):v for k,v in arrays.items()})
for cohort in ('legacy','gsm8k'):
summary[cohort]={}
for metric in ('distance','divergence','QKV'):
item={role:{name:stats(arrays[(cohort,role,metric,name)]) for name in ('effective_heads','effective_fraction','concentration','top1','top25','bottom25','heads90','total')} for role in ('base','finetuned')}
before=arrays[(cohort,'base',metric,'effective_heads')];after=arrays[(cohort,'finetuned',metric,'effective_heads')];delta=after-before
item['paired']={'effective_heads_delta':stats(delta),'increased_fraction':float(np.mean(delta>0)),'decreased_fraction':float(np.mean(delta<0)),'winner_agreement':float(np.mean(arrays[(cohort,'base',metric,'winner')]==arrays[(cohort,'finetuned',metric,'winner')]))}
summary[cohort][metric]=item
recipients=np.concatenate(pool[(cohort,'base',metric,'recipient')]);names=np.concatenate(pool[(cohort,'base',metric,'sample')])
for layer in (3,7,11,15,19,23):
which=recipients==layer;a=stats(before[which]);b=stats(after[which]);d=stats(delta[which]);layer_rows.append({'cohort':cohort,'metric':metric,'recipient':layer,'base_median':a['median'],'base_p10':a['p10'],'base_p90':a['p90'],'finetuned_median':b['median'],'finetuned_p10':b['p10'],'finetuned_p90':b['p90'],'paired_delta_median':d['median']})
for sample in samples[cohort]:
which=names==sample['id'];item.setdefault('per_text',{})[sample['id']]={'base':stats(before[which]),'finetuned':stats(after[which]),'paired_delta':stats(delta[which])}
expected=(8.93182211792415,5.7840811612328125,11.67265834734814);b=summary['legacy']['distance']['base']['effective_heads'];np.testing.assert_allclose([b[k] for k in ['median','p10','p90']],expected,rtol=1e-10)
(root/'summary.json').write_text(json.dumps(summary,indent=2)+'\n');csvwrite(root/'per-text-layer.csv',rows);csvwrite(root/'per-layer.csv',layer_rows)
figs=root/'figures';figs.mkdir(exist_ok=True)
def save(fig,name):
for ext in ('png','pdf','svg'):fig.savefig(figs/(name+'.'+ext),dpi=180)
plt.close(fig)
fig,axs=plt.subplots(1,2,figsize=(13,4.8),layout='constrained')
for ax,cohort in zip(axs,['legacy','gsm8k']):
for role,label,color in [('base','Original release','C0'),('finetuned','GSM8K GRPO LoRA seed0','C1')]:
x=np.sort(arrays[(cohort,role,'distance','effective_heads')]);ax.plot(x,np.arange(1,len(x)+1)/len(x),label=label,color=color)
ax.set(title='Original four texts' if cohort=='legacy' else 'Four GSM8K held-out question prefixes',xlabel='Effective donor heads: sqrt(JS) influence',ylabel='Empirical CDF',xlim=(1,16),ylim=(0,1));ax.grid(alpha=.2);ax.legend()
fig.suptitle('Same inputs before/after existing task fine-tuning; 32 tokens; one training seed; no training here');save(fig,'paired_effective_heads_cdf')
fig,axs=plt.subplots(1,2,figsize=(13,4.8),layout='constrained')
for ax,cohort in zip(axs,['legacy','gsm8k']):
for role,prefix,color in [('base','base','C0'),('finetuned','finetuned','C1')]:
rr=[r for r in layer_rows if r['cohort']==cohort and r['metric']=='distance'];x=[r['recipient'] for r in rr];med=[r[prefix+'_median'] for r in rr];lo=[r[prefix+'_p10'] for r in rr];hi=[r[prefix+'_p90'] for r in rr];ax.plot(x,med,'o-',color=color,label=role);ax.fill_between(x,lo,hi,color=color,alpha=.12)
ax.set(title=cohort,xlabel='Full-attention recipient layer index',ylabel='Effective donor heads: median / P10–P90',ylim=(1,16));ax.legend();ax.grid(alpha=.2)
fig.suptitle('All six full-attention receivers, sixteen donor heads each; descriptive dependent observations');save(fig,'paired_effective_heads_by_layer')
fig,axs=plt.subplots(1,2,figsize=(13,4.8),layout='constrained')
for ax,cohort in zip(axs,['legacy','gsm8k']):
delta=arrays[(cohort,'finetuned','distance','effective_heads')]-arrays[(cohort,'base','distance','effective_heads')];ax.hist(delta,bins=60,density=True,alpha=.7);ax.axvline(0,color='black',linestyle='--');ax.axvline(np.median(delta),color='C3',label='Paired median');ax.set(title=cohort,xlabel='Finetuned minus original effective head count',ylabel='Density');ax.legend()
fig.suptitle('Paired changes; positive = more distributed, negative = more concentrated');save(fig,'paired_effective_heads_delta')
lines=['# GSM8K任务微调前后的有效头数','', '比较原Qwen3.5-2B发布权重与同一revision上的纯LoRA seed0(额外GSM8K GRPO,非SFT)。没有训练新模型。主指标为√JS距离消融影响的N_eff;正变化表示更分散,负变化表示更集中。','', '| 输入组 | 原权重中位数 [P10,P90] | 微调后中位数 [P10,P90] | 配对变化中位数 | 增加的观测比例 |','|---|---:|---:|---:|---:|']
for cohort,label in [('legacy','原四条文本'),('gsm8k','GSM8K测试题前缀')]:
item=summary[cohort]['distance'];a=item['base']['effective_heads'];b=item['finetuned']['effective_heads'];d=item['paired']['effective_heads_delta'];lines.append(f"| {label} | {a['median']:.6f} [{a['p10']:.6f},{a['p90']:.6f}] | {b['median']:.6f} [{b['p10']:.6f},{b['p90']:.6f}] | {d['median']:+.6f} | {item['paired']['increased_fraction']:.1%} |")
lines+=['', '每组4条输入×6个接收层×25个query位置×8个接收query头=4800个配对观测;它们不是4800个独立文本样本。前后使用同一原模型tokenizer、完全相同token IDs。原四条输入的基线8.931822/16已精确复现。','', 'GSM8K输入取原作者固定版本test split中前4道至少32token的题目,仅用题目前32token,不含答案,不套不同聊天模板。另算了同组GSM8K输入的原权重基线;没有把旧通用文本与新任务文本混作配对。','', '## 方法与限定','', '所有6个普通注意力接收层和16个上游输出头均保留;每次移除一个上游头在全序列的投影输出,重算中间MLP/残差/归一化和检索分布。微调只改变18个GDN层的指定投影,普通注意力权重不变;隐藏状态和上游贡献仍会变化。','', '采用read-only FP32有效权重W+2BA,未把增量舍入为BF16、未修改底座;另用base projection+2B(Ax)独立前向校验。头输出求和、空消融和源数组复用校验均记录。JS散度及QKV复合相对变化也保留,避免只展示有利度量。这里QKV补充指标仅覆盖相同六个普通注意力接收层,其原基线不能与旧研究全部23连接的76.0%混作同一统计量。','', '单个发布者训练seed、4条短文本/组,仅支持这些输入上的描述性变化;不推广为所有微调都会如此,不报告GSM8K准确率,不据此推断删头可用或最终LM能力。配对变化中位数与两个总体中位数之差不是同一个统计量。','', '[公开微调权重与训练说明](https://huggingface.co/Avifenesh/gdn-beta-gate-qwen3.5-2b-grpo);固定版本、参数SHA、dataset版本和题目索引见protocol.json/gsm8k-source.json/samples.json。所有权重、适配器和缓存继续保留。']
(root/'REPORT.md').write_text('\n'.join(lines)+'\n');print((root/'REPORT.md').read_text(),end='')
if __name__=='__main__':main(sys.argv[1])