3、权重设计与归一化:特征权重分配、归一化处理、稀有度分数计算、排序算法

好,咱们进入第三章。这一章可以说是整个稀有度算法的「心脏」。前面两章我们聊了特征怎么存、随机数怎么生成,但真正决定一个NFT「值不值钱」的,其实是权重设计和归一化处理。

我最早做的一个项目,团队里大家拍脑袋给特征定了权重,结果上链后才发现——某个「普通背景」因为权重设得太高,反而比稀有特征还难出。嗯,那叫一个惨。所以这一章,我会把我在多个项目里踩过的坑、总结出的经验,全都掰开揉碎了讲给你听。

3.1 特征权重分配:凭什么这个特征更稀有?

权重分配,说白了就是给每个特征「打分」。你想想看,一个NFT头像里,金色皇冠肯定比蓝色帽子稀有,对吧?那这个「稀有」的程度,就需要用数字量化出来。

我个人习惯把权重分为三个层级:

  • 基础权重:每个特征类别的基础分,比如背景、衣服、帽子各占多少比例
  • 特征内权重:同一类别下,不同具体特征的稀有度,比如「金色皇冠」权重0.5%、「蓝色帽子」权重15%
  • 组合权重:某些特征同时出现时的加成,比如「金色皇冠+红色披风」这种组合,权重可以额外调整

核心原则:所有权重加起来必须等于100%。这是铁律,少一分多一分都会导致生成逻辑出错。

我在项目中遇到过最头疼的问题,就是团队里设计师和产品经理对权重各执一词。设计师觉得「这个配色很高级,应该稀有」,产品经理觉得「这个元素用户喜欢,应该多发」。最后我定了个规矩:权重由数据驱动,而不是感觉驱动。先跑一轮小规模测试,看市场反馈再调。

3.2 归一化处理:把权重变成「公平的分数」

权重分配好了,但直接拿原始权重去算稀有度,会出大问题。为什么?因为不同特征类别的权重范围可能差很多。比如「背景」的权重范围是0.1%~30%,而「眼睛」的权重范围是1%~20%。直接比较的话,背景类里的低权重特征会被「淹没」。

归一化,就是把这些不同量纲的数据,统一映射到0到1之间。我常用的方法有两种:

方法 公式 适用场景
Min-Max归一化 (x - min) / (max - min) 权重分布比较均匀时
Z-Score标准化 (x - mean) / std 有权重异常值(比如某个特征权重特别低)

我个人更推荐Min-Max归一化,因为它简单直观,而且结果直接对应稀有度百分比。你想想看,归一化后的0.8,就意味着这个特征比80%的其他特征更稀有。

小技巧:归一化时记得保留原始权重。我习惯在数据结构里同时存「raw_weight」和「normalized_weight」,方便后续调试和回溯。

3.3 稀有度分数计算:一个NFT到底有多稀有?

好,权重归一化完了,接下来就是算每个NFT的稀有度分数。这里我见过很多新手直接把所有特征的归一化权重加起来——其实这是错的。

正确的做法是:稀有度分数 = 各特征归一化权重的乘积。为什么是乘积而不是求和?因为稀有度是「所有特征同时出现」的概率,概率论告诉我们,独立事件同时发生的概率是相乘的。

举个例子:

// 一个NFT有三个特征
特征A(背景:金色)归一化权重:0.02
特征B(帽子:皇冠)归一化权重:0.01
特征C(眼睛:激光)归一化权重:0.05

// 错误做法(求和)
稀有度分数 = 0.02 + 0.01 + 0.05 = 0.08

// 正确做法(乘积)
稀有度分数 = 0.02 * 0.01 * 0.05 = 0.00001

看到了吗?乘积的结果更能体现「同时出现」的稀有程度。0.00001意味着这个组合出现的概率是十万分之一,这才是真正的稀有。

我曾经踩过的坑:有一次我直接用乘积算分数,结果发现所有NFT的分数都集中在0.000001到0.0001之间,数值太小,排序时精度不够。后来我加了一步「对数变换」:score = -log(product),这样分数越大越稀有,而且数值范围更友好。

3.4 排序算法:谁是最稀有的那个?

分数算出来了,接下来就是排序。排序本身不难,但要注意几个细节:

  • 稳定排序:如果两个NFT分数相同,按生成顺序排,不要随机打乱
  • 排名与分数脱钩:排名是排名,分数是分数。我见过有人把排名直接当分数用,结果两个NFT分数差10倍,排名只差1位,这显然不合理
  • 百分位排名:我习惯把排名转换成百分位,比如「前1%稀有」「前10%稀有」,这样用户更容易理解

排序的代码实现其实很简单,但链上排序就有点讲究了。因为Solidity里不能直接用数组排序(gas费太高),我一般会在链下算好排名,然后通过Merkle树或预言机传到链上。

// Python链下排序示例
def calculate_rarity(nfts):
    # 先算分数
    for nft in nfts:
        score = 1.0
        for trait in nft['traits']:
            score *= trait['normalized_weight']
        nft['score'] = -math.log(score)  # 对数变换
    
    # 再排序
    nfts.sort(key=lambda x: x['score'], reverse=True)
    
    # 最后赋排名
    for i, nft in enumerate(nfts):
        nft['rank'] = i + 1
        nft['percentile'] = (i + 1) / len(nfts) * 100

关键点:链下算好排名后,一定要做一次「排名验证」。我习惯在合约里存一个排名哈希,用户查询时,链下提供排名证明,合约验证哈希是否匹配。这样既省gas,又保证排名不可篡改。

3.5 实战中的那些坑

最后,分享几个我在真实项目中遇到的坑,你以后大概率也会碰到:

  1. 权重总和不是100%:有一次我手滑,把某个特征的权重写成了0.5%,结果总和变成了99.5%。生成到第1000个NFT时,随机数用完了——因为概率分布不完整。从那以后,我每次部署前都会写个单元测试,检查所有权重总和是否为100%。
  2. 归一化后精度丢失:Solidity不支持浮点数,归一化后的0.0001在合约里只能存成整数。我一般乘以10^6再取整,这样精度够用,gas费也合理。
  3. 排序结果被「刷」:有些用户会通过批量铸造来「刷」稀有度。我的对策是:在排序算法里加入「铸造时间权重」,越早铸造的NFT,在分数相同时排名更高。这样既公平,又鼓励早期参与。

嗯,这一章的内容就到这里。权重设计、归一化、分数计算、排序——这四个步骤环环相扣,少一步都不行。下一章我们会聊「链上随机数生成」,到时候我会把VRF、Commit-Reveal这些方案都讲透。咱们下章见。