74 lines
9.3 KiB
Python
74 lines
9.3 KiB
Python
"""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])
|