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

141 lines
16 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.
"""Saved-data scientific analysis; no model invocation or fitting."""
import csv,json
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from .core import concentration
def stats(x):
x=np.asarray(x,dtype=np.float64);valid=x[np.isfinite(x)]
if not valid.size:return {'count':0,'p10':None,'median':None,'p90':None,'mean':None}
q=np.quantile(valid,[.1,.5,.9]);return {'count':int(valid.size),'p10':float(q[0]),'median':float(q[1]),'p90':float(q[2]),'mean':float(valid.mean())}
def csvwrite(p,rows):
with p.open('w') as f:
w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
def main(root):
root=Path(root);figdir=root/'figures';figdir.mkdir(exist_ok=True);status=json.loads((root/'status.json').read_text());assert status['state']=='completed'
rows=[];joint_rows=[];pool={};curves={};joint_pool={};obs={};winner=[];joint_by_layer={}
for entry in status['completed_links']:
sample=entry['sample'];source=entry['source'];data=dict(np.load(root/'raw'/f'{sample}-link-{source:02d}.npz'))
if 'single_JS' in data:
data['single_sqrtJS']=np.sqrt(data['single_JS'])
for label in ('weak_half','strong_half','random_half','all_heads'):
data['joint_'+label+'_sqrtJS']=np.sqrt(data['joint_'+label+'_JS'])
masks=data.get('mask_'+label,np.ones_like(data['mask_weak_half']))
data['individual_sum_'+label+'_sqrtJS']=np.sum(data['single_sqrtJS'].transpose(2,1,0)*masks,-1).T
for key in ('Q','K','V','QKV','JS','sqrtJS'):
if 'single_'+key not in data:continue
d=data['single_'+key][:,7:,:].transpose(1,2,0);c=concentration(d);valid=c['valid'];h=d.shape[-1]
metric={k:v.reshape(-1) for k,v in c.items() if k!='valid'}
row={'sample':sample,'source':source,'recipient':source+1,'metric':key,'source_heads':h,'recipient_attention_type':'full_attention' if entry['full_attention_recipient'] else 'linear_attention','observations':d.shape[0]*d.shape[1],'undefined_zero_total':int((~valid).sum())}
for k,v in metric.items():
pool.setdefault(key,{}).setdefault(k,[]).extend(v)
for s,z in stats(v).items():row[k+'_'+s]=z
rows.append(row)
# Raw per-observation statistics for independently reproducing figures.
for k,v in metric.items():obs.setdefault(key+'_'+k,[]).append(v.astype(np.float32))
n=d.shape[0]*d.shape[1]
for k,v in [('source',np.full(n,source)),('sample_index',np.full(n,sorted({e['sample'] for e in status['completed_links']}).index(sample))),('token',np.repeat(np.arange(7,32),d.shape[1])),('recipient_head',np.tile(np.arange(d.shape[1]),25))]:obs.setdefault(key+'_'+k,[]).append(v.astype(np.int16))
p=np.divide(d,d.sum(-1,keepdims=True),out=np.zeros_like(d),where=d.sum(-1,keepdims=True)>0);cum=np.concatenate([np.zeros((*d.shape[:2],1)),np.cumsum(np.sort(p,-1)[...,::-1],-1)],-1)
grid=np.linspace(0,1,101);cc=np.array([np.interp(grid,np.linspace(0,1,h+1),v) for v in cum[valid]])
curves.setdefault(key,[]).extend(cc)
win=np.argmax(d,-1);counts=np.bincount(win[valid],minlength=h)
winner.append({'sample':sample,'source':source,'metric':key,'most_frequent_winner_fraction':float(counts.max()/max(counts.sum(),1)),'distinct_winning_heads':int((counts>0).sum()),'head_counts':json.dumps(counts.tolist())})
for label in ('weak_half','strong_half','random_half','all_heads'):
values=data['joint_'+label+'_'+key][7:];summed=data['individual_sum_'+label+'_'+key][7:]
jr={'sample':sample,'source':source,'recipient':source+1,'metric':key,'removed':label}
for k,x in [('joint_effect',values),('sum_individual',summed)]:
for s,z in stats(x).items():jr[k+'_'+s]=z
joint_rows.append(jr);joint_pool.setdefault(key,{}).setdefault(label,[]).extend(values.ravel());joint_by_layer.setdefault((key,label,source),[]).extend(values.ravel())
csvwrite(root/'concentration-by-link.csv',rows);csvwrite(root/'joint-by-link.csv',joint_rows);csvwrite(root/'winner-identities.csv',winner)
np.savez_compressed(root/'observations.npz',**{k:np.concatenate(v) for k,v in obs.items()})
pooled={k:{m:stats(v) for m,v in metrics.items()} for k,metrics in pool.items()};joint_stats={k:{m:stats(v) for m,v in metrics.items()} for k,metrics in joint_pool.items()}
paired={}
for key,labels in joint_pool.items():
weak=np.asarray(labels['weak_half']);strong=np.asarray(labels['strong_half']);random=np.asarray(labels['random_half']);valid=weak>0
paired[key]={'weak_less_than_strong_fraction':float(np.mean(weak<strong)),'weak_less_than_random_fraction':float(np.mean(weak<random)),'zero_weak_effect_count':int((~valid).sum()),'strong_over_weak':stats(strong[valid]/weak[valid]),'random_over_weak':stats(random[valid]/weak[valid])}
full_sources=sorted({x['source'] for x in status['completed_links'] if x['full_attention_recipient']})
by_type={}
for key in pool:
src=np.concatenate(obs[key+'_source']);by_type[key]={}
for label,choose in [('full_attention',np.isin(src,full_sources)),('linear_attention',~np.isin(src,full_sources))]:
if not choose.any():continue
by_type[key][label]={m:stats(np.concatenate(obs[key+'_'+m])[choose]) for m in pool[key]}
summary={'scale_sensitivity':'sqrt(JS) added after reviewing completed primary summaries; primary JS and all raw interventions unchanged; squared divergences cannot be equated to probability mass or linear-scale effects','by_recipient_type':by_type,'full_attention_source_indices':full_sources,'paired_joint':paired,'pooling':'query7..31, per text/token/recipient-query-head; descriptive dependent observations, not independent samples','concentration':pooled,'joint':joint_stats,'counts':{'links':len(status['completed_links']),'texts':len(json.loads((root/'samples.json').read_text())),'layer_transitions':23},'sample_index_key':sorted({e['sample'] for e in status['completed_links']}),'winner_identity_file':'winner-identities.csv'}
(root/'analysis-summary.json').write_text(json.dumps(summary,indent=2,allow_nan=False)+'\n')
def save(fig,name):
for ext in ('png','pdf','svg'):fig.savefig(figdir/(name+'.'+ext),dpi=180)
plt.close(fig)
fig,axs=plt.subplots(1,2,figsize=(14,4.8),layout='constrained')
colors={'Q':'C0','K':'C1','V':'C2','QKV':'C3'}
for key in colors:
for ax,metric in zip(axs,['effective_fraction','top1']):
med=[];lo=[];hi=[]
source= np.concatenate(obs[key+'_source']);vals=np.concatenate(obs[key+'_'+metric])
for layer in range(23):
z=stats(vals[source==layer]);lo.append(z['p10']);med.append(z['median']);hi.append(z['p90'])
ax.plot(range(1,24),med,label=key,color=colors[key]);ax.fill_between(range(1,24),lo,hi,color=colors[key],alpha=.10)
axs[0].set(title='Continuous effective-head fraction (median, p10–p90)',ylabel='N_eff / number of donor heads')
axs[1].set(title='Largest donor-head influence share',ylabel='Top-1 normalized ablation effect')
for ax in axs:
for j,source in enumerate(full_sources):ax.axvspan(source+.6,source+1.4,color='gray',alpha=.08,label='Full-attention receiver' if j==0 else None)
ax.set(xlabel='Recipient layer index',ylim=(0,1.02));ax.legend();ax.grid(alpha=.2)
fig.suptitle('All 23 adjacent transitions; four texts, query positions 7–31; no training')
save(fig,'continuous_dominance')
fig,axs=plt.subplots(1,2,figsize=(13,4.8),layout='constrained');curve_rows=[]
for key in colors:
a=np.asarray(curves[key]);lo,med,hi=np.quantile(a,[.1,.5,.9],axis=0);x=np.linspace(0,1,101)
axs[0].plot(x,med,label=key,color=colors[key]);axs[0].fill_between(x,lo,hi,color=colors[key],alpha=.1)
for j,xx in enumerate(x):curve_rows.append({'metric':key,'fraction_ranked_heads':xx,'p10':lo[j],'median':med[j],'p90':hi[j]})
v=np.sort(np.asarray(pool[key]['effective_fraction']));axs[1].plot(v,np.arange(1,len(v)+1)/len(v),label=key,color=colors[key])
axs[0].plot([0,1],[0,1],':',color='gray',label='Uniform');axs[0].set(title='Influence concentration curve',xlabel='Fraction of donor heads, largest first',ylabel='Cumulative normalized single-head effects')
axs[1].set(title='Continuous effective-head fraction: empirical CDF',xlabel='N_eff / donor heads',ylabel='Fraction of observed cases')
for ax in axs:ax.set(xlim=(0,1),ylim=(0,1.02));ax.legend();ax.grid(alpha=.2)
fig.suptitle('Observation-wise ranking; dominant identities can differ; shaded p10–p90')
csvwrite(root/'concentration-curve.csv',curve_rows);save(fig,'ranked_influence_distribution')
fig,axs=plt.subplots(1,2,figsize=(14,4.8),layout='constrained')
for label,color in zip(['weak_half','strong_half','random_half','all_heads'],['C0','C3','C1','gray']):
for ax,key in zip(axs,['QKV','JS']):
selected=[r for r in joint_rows if r['metric']==key and r['removed']==label]
xs=sorted(set(r['recipient'] for r in selected));ys=[np.median(joint_by_layer[(key,label,x-1)]) for x in xs]
ax.plot(xs,np.maximum(ys,1e-12),'o-',label=label,color=color,markersize=4)
axs[0].set(title='Joint removal: balanced QKV relative effect',ylabel='Median token/head relative L2 score')
axs[1].set(title='Full-attention recipients: retrieval change',ylabel='Median token/head JS divergence (nats)')
for ax in axs:ax.set(xlabel='Recipient layer index',yscale='log');ax.legend();ax.grid(alpha=.2)
fig.suptitle('Equal-count weak/strong/random removal; token-wise masks; log display floor 1e-12')
save(fig,'joint_removal_effects')
fig,axs=plt.subplots(1,2,figsize=(13,4.8),layout='constrained')
for key,label,color in [('JS','JS divergence (squared scale)','C4'),('sqrtJS','sqrt(JS) distance','C5')]:
a=np.asarray(curves[key]);lo,med,hi=np.quantile(a,[.1,.5,.9],axis=0);x=np.linspace(0,1,101)
axs[0].plot(x,med,label=label,color=color);axs[0].fill_between(x,lo,hi,color=color,alpha=.12)
for j,xx in enumerate(x):curve_rows.append({'metric':key,'fraction_ranked_heads':xx,'p10':lo[j],'median':med[j],'p90':hi[j]})
v=np.sort(np.asarray(pool[key]['effective_fraction']));axs[1].plot(v,np.arange(1,len(v)+1)/len(v),label=label,color=color)
axs[0].plot([0,1],[0,1],':',color='gray',label='Uniform');axs[0].set(title='Retrieval influence rank curve',xlabel='Fraction of donor heads, largest first',ylabel='Cumulative normalized intervention effects')
axs[1].set(title='Sensitivity to divergence versus distance scale',xlabel='Effective donor-head fraction',ylabel='Fraction of observations')
for ax in axs:ax.set(xlim=(0,1),ylim=(0,1.02));ax.legend();ax.grid(alpha=.2)
fig.suptitle('Full-attention recipients; sqrt(JS) is post-summary scale sensitivity, not a new model run')
csvwrite(root/'concentration-curve.csv',curve_rows);save(fig,'retrieval_scale_sensitivity')
lines=['# 上游注意力头对下一层 Q/K/V 的影响集中度','', '实测全部23个相邻层连接、4条既有中英文文本各32 token。主统计取query7–31,按文本/token/接收query头分别计算,再汇总。不是独立样本的显著性检验。','', '影响是移除上游一个头后,下一层Q/K/V向量的相对L2变化;重新计算中间MLP/残差/归一化。各头影响归一化仅构成消融影响分布,不是天然可相加的贡献概率。','', '| 对象 | 有效头比例中位数 [P10,P90] | 最大头份额中位数 | 前25%头份额中位数 | 最小25%头份额中位数 | 90%影响所需头数中位数 |','|---|---:|---:|---:|---:|---:|']
for k in ('Q','K','V','QKV','JS','sqrtJS'):
if k not in pooled:continue
z=pooled[k];a=z['effective_fraction'];lines.append(f"| {k} | {a['median']:.3f} [{a['p10']:.3f},{a['p90']:.3f}] | {z['top1']['median']:.1%} | {z['top25']['median']:.1%} | {z['bottom25']['median']:.1%} | {z['heads90']['median']:.1f} |")
lines+=['','sqrtJS为JS距离尺度敏感性分析:在初次完整汇总后增加,原始消融和原JS主结果不变。JS散度近似平方尺度,归一化后会放大强弱差距;JS影响份额不是注意力概率质量份额。','', '按接收层类型分层(避免总体分布混合不同机制):','', '| 接收层类型 | Q有效头比例中位数 | K | V | QKV |','|---|---:|---:|---:|---:|']
for label in ('linear_attention','full_attention'):
lines.append('| '+label+' | '+' | '.join(format(by_type[k][label]['effective_fraction']['median'],'.3f') for k in ('Q','K','V','QKV'))+' |')
lines+=['','QKV是三个相对变化平方的均值再开根号,避免混加不同Q/K/V尺度;JS仅普通注意力接收层有。普通注意力GQA的K/V复制到query头索引用于配对,不是额外独立KV头。','', '| 联合移除 | QKV相对影响中位数 [P10,P90] | 检索JS中位数 [P10,P90] |','|---|---:|---:|']
for label in ('weak_half','strong_half','random_half','all_heads'):
a=joint_stats['QKV'][label];b=joint_stats['JS'][label];lines.append(f"| {label} | {a['median']:.5f} [{a['p10']:.5f},{a['p90']:.5f}] | {b['median']:.6f} [{b['p10']:.6f},{b['p90']:.6f}] |")
lines+=['', '配对观测:QKV弱半数的实际联合影响小于强半数的比例为'+format(paired['QKV']['weak_less_than_strong_fraction'],'.1%')+';强/弱影响比中位数为'+format(paired['QKV']['strong_over_weak']['median'],'.3f')+'。这是逐观测比值的中位数,不是两个总体中位数之比。弱效应为零的观测另计,不做除法。', '', '联合实验的弱/强/随机半数头,在各token、各接收头上分别选择,形成完整序列再重算。排名依据单头QKV影响,同样用于JS;它是输入自适应诊断,不是免费可用的剪枝策略。线性注意力的卷积和普通注意力的QK可能传播跨token影响。所有上游头移除仍保留残差背景,不能把它称为删除全部上游信息。', '', '连续指标:N_eff=1/Σp²;有效头比例=N_eff/H;C=(Σp²−1/H)/(1−1/H)。全零总影响时集中度未定义,原始数组保留NaN并在统计时排除、另计数量。单头相对L2影响在做头间占比归一化之前的总和及最大值均保留,避免把微小效应中的集中误认作重要。字段max_absolute指最大无符号相对效应,并非隐藏坐标中的未归一化L2;total是单头效应之和,真正全头联合效果另测。', '', '联合实验中保存的单头影响之和仅为按query掩码加权的参考。单头消融移除整段序列的一个头,联合掩码则可跨位置变化;二者的差不能单独解释成纯非线性交互,因为时间支持也不同。应直接比较实际弱/强/随机联合效果。', '', '## 文件与限制','', '`raw/*.npz`保留每次单头影响、联合影响、相应单头影响之和、删除掩码及空操作对照;`observations.npz`保留逐观测连续指标及索引;CSV和JSON为全部图表源表。原模型权重和大基线缓存继续保留在服务器。','', '只测短文本上的局部下一层作用;不代表全LM质量、可直接删头、所有输入普遍集中,或相似头可互相替代。所有头贡献可能是广泛分布的;报告不以发现稀疏为成功条件。','',f"CPU测量耗时{status['elapsed_seconds']:.1f}秒,峰值RSS{status['peak_rss_kib']/1024:.1f} MiB;无训练、GPU或清理。"]
(root/'REPORT.md').write_text('\n'.join(lines)+'\n')
print((root/'REPORT.md').read_text())
if __name__=='__main__':
import sys
main(sys.argv[1])