一起聊聊设计原则

今晚我们一起来聊聊关于设计原则相关的知识点。

SOLID五大原则是什么

SRP 单一责任原则

单一责任原则,从名字上我们就能比较好的去理解它。这项原则主张一个对象只专注于单个方面的逻辑,强调了职责的专一性。

举个例子:
学生管理系统中,我们需要提交一些学生的基本资料,那么学生信息相关的程序都交给了StudentService负责,如果我们要实现一个保存教师基本资料的功能就应该新建一个TeacherService去处理,而不应该写在StudentService当中。

OCP开放封闭原则

这项原则从我个人的角度去理解,它更加强调的是对于扩展的开放性,例如当我们需要调整某些实现逻辑的时候,尽量不要直接改动到原有的实现点。

但是这里面有几个点容易被人们误解:
第一点
开放封闭原则虽然强调的是不要随意改动代原先代码到逻辑结构,但是并没有要求一定不能对代码进行改动!

第二点
同样是代码改动,如果我们可以从功能,模块的角度去看,实际上代码的改动更多地可以被认作为是一种“扩展”。
关于如何做到开放封闭原则,下文我会专门用一个案例来进行介绍。

LSP里氏替换原则

里氏替换原则强调的是不能破坏一个原有类设计的原始设计体系。强调了子类可以对父类程序进行继承。但是有几个点需要注意下:

如果父类定义的规则最好是最基础,必须遵守的法则。如果子类继承了父类之后,在某个方法的实现上违背了初衷,那么这样的设计就是违背了里氏替换法则。
例如:
父类的设计是希望实现商品库存扣减的功能,但是子类的实现却是实现了库存+1的功能,这就很明显是牛头不对马嘴了。

子类不要违背父类对于入参,出参,异常方面的约定。例如:父类对于异常的抛出指定的是 NullPointException ,但是子类却在实现的时候声明了会出 illegalArgumentException,那么此时就需要注意到设计已经违背了LSP原则。

同样,具体的案例我在下文会列举出来和大家进行代码分享。

ISP接口隔离原则

理解“接口隔离原则”的重点是理解其中的“接口”二字。
这里有三种不同的理解。如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。

如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。

DIP依赖倒置原则

比较经典的例子,例如说Spring框架的IOC控制反转,将bean的管理交给了Spring容器去托管。依赖注入则是指不通过明确的new对象的方式来在类中创建类,而是提前将类创建好,然后通过构造函数,setter函数等方式将对应的类注入到所需使用的对象当中。
DIP的英文解释大致为:

High-level modules shouldn’t depend on low-level modules.
Both modules should depend on abstractions. In addition,
abstractions shouldn’t depend on details. Details depend on
abstractions.

解释过来就是,高层次的模块不应该依赖低层次的模块,不同的模块之间应该通过接口来互相访问,而并非直接访问到对方的具体实现。
清楚了这么多理论知识之后,接下来我们通过一些代码实战案例来进行更加深入的了解吧。

代码实战理解设计原则

单一责任原则案例

我们来看这么一个类,简单的一个用户信息类中,包含了一个叫做home的字段,这个字段主要用于记录用户所居住的位置。

/**
 * @Author linhao
 * @Date created in 7:22 上午 2021/9/3
 */
public class UserInfo {
    
    private String username;
    private short age;
    private short height;
    private String phone;
    private String home;
    
}

慢慢地随着业务的发展,这个实体类中的home字段开始进行了扩展,UserINfo类变成了以下模式:

/**
 * @Author linhao
 * @Date created in 7:22 上午 2021/9/3
 */
public class UserInfo {private String username;
    private short age;
    private short height;
    private String phone;
    private String home;
    /**
     * 省份
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 地区
     */
    private String region;
    /**
     * 街道
     */
    private String street;
}


此时对于这个实体类的设计就会有了新的观点:
这个类中关于居住部分的字段开始渐渐增加,应该将住址部分抽象出来成一个Address字段,拆分后变成如下所示:


/**
 * @Author linhao
 * @Date created in 7:22 上午 2021/9/3
 */
public class UserInfo {private String username;
    private short age;
    private short height;
    private String phone;
    private String home;
    /**地址信息**/
    private Address address;
    
}

这样的拆分可以确保UserInfo对象的职责单一,类似的扩展还可以蔓延到后续的email,tel相关属性。

举这个例子只是想简单说明,我们在对一些类进行设计的时候,其实就已经使用到了单一责任原则。另外还有可能在以下场景中也有运用到该原则:

类中的属性字段特别多,一个bean中充斥了几十个属性。此时也可以尝试使用单一责任原则,将不同属性的字段归纳为一个bean进行收拢。
一个大对象,例如XXXManager或者XXXContext这种名词定义的对象中,可能引入了一大堆的外部依赖,此时可以按照依赖的类别来进行拆分。
业务代码块中,我们定义了一个UserService类,然后这个类里面写了一坨的用户密码,手机号,身份证号解密加密相关的私有函数,这时候可以不妨尝试将这些私有方法统统抽象成为一个独立的Util当中,从而减少UserService中的代码量。

所以最终你会发现,单一责任原则还是一个比较需要依靠主观意识去拿捏的一项技巧。随着我们实践开发经验的逐渐提升,自然就会明白什么样的代码该进行良好的抽象与优化了。

开放封闭原则案例

关于这条原则我个人感觉要想较好地理解它,需要有具体的实战案例代码,所以接下来我打算用一个自己曾经在工作中遇到的实际场景和你分享:

我做的一款社交小程序应用当中,当一个用户注册完信息之后,需要通知到系统下游,主要是修改某些后台数据,分配对应的员工去跟进这个用户。
所以大体的代码设计可能如下所示:

public class RegisterHandler {public void postProcessorAfterRegister(long userId){
        //通知员工
        notifyWorker(userId);
    }
    
    private void notifyWorker(long userId){
        //通知部分的逻辑
    }
}public interface IRegisterHandler {/**
     * 用户注册之后处理函数
     *
     * @param userId 用户渠道ID
     */
    void postProcessorAfterRegister(long userId);
}

但是注册的渠道类型有许多种,例如公众号,小程序二维码传播,小程序的分享链接,其他App渠道等等。所以代码结构需要做部分调整:
首先需要修改一开始设计的接口模型:

/**
 * @Author linhao
 * @Date created in 7:56 上午 2021/9/3
 */
public interface IRegisterHandler {/**
     * 用户注册之后处理函数
     *
     * @param userId 用户ID
     * @param sourceId 注册渠道ID
     */
    void postProcessorAfterRegister(long userId,int sourceId);
}

然后还需要修改实际的实现规则:

/**
 * @Author linhao
 * @Date created in 7:48 上午 2021/9/3
 */
public class RegisterHandler implements IRegisterHandler {
​
​
    @Override
    public void postProcessorAfterRegister(long userId, int sourceId) {
        //通知员工
        if (sourceId == 1) {
            //doSth
        } else if (sourceId == 2) {
            //doSth
        } else if (sourceId == 3) {
            //doSth
        } else {
            //doSth
        }
        notifyWorker(userId, sourceId);
    }
​
​
    private void notifyWorker(long userId, int sourceId) {
        //通知部分的逻辑
    }
}

这样的代码扩展就会对原先定义好的结构造成破坏,也就不满足我们所认识的开放封闭原则了。(虽然我在上文中有提及过对于开放封闭原则来说,并不是强制要求不对代码进行修改,但是现在的这种扩展模式已经对内部结构造成了较大的伤害。)

所以我们可以换一种设计思路去实现。
首先我们需要将注册的传入参数定义为一个对象类型,这样在后续新增参数的时候只需调整对象内部的字段即可,不会对原有接口的设计造成影响:

/**
 * @Author linhao
 * @Date created in 8:07 上午 2021/9/3
 */
public class RegisterInputParam {private long userId;private int source;public long getUserId() {
        return userId;
    }public void setUserId(long userId) {
        this.userId = userId;
    }public int getSource() {
        return source;
    }public void setSource(int source) {
        this.source = source;
    }
}

接着可以将注册逻辑拆解为注册处理器和使用注册处理器的service模块:

/**
 * @Author linhao
 * @Date created in 7:56 上午 2021/9/3
 */
public interface IRegisterService {/**
     * 用户注册之后处理函数
     *
     * @param registerInputParam 用户注册之后的传入参数
     */
    void postProcessorAfterRegister(RegisterInputParam registerInputParam);
}

注册处理器内部才是真正的核心部分:

/**
 * @Author linhao
 * @Date created in 8:10 上午 2021/9/3
 */
public abstract class AbstractRegisterHandler {/**
     * 获取注册渠道ID
     *
     * @return
     */
    public abstract int getSource();/**
     * 注册之后的核心通知模块程序
     *
     * @param registerInputParam
     * @return
     */
    public abstract boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam);}

具体的实现交给了各个Handler组件:
公众号注册渠道的后置处理器

/**
 * @Author linhao
 * @Date created in 8:16 上午 2021/9/3
 */
public class GZHRegisterHandler  extends AbstractRegisterHandler {

    @Override
    public int getSource() {
        return RegisterConstants.RegisterEnum.GZH_CHANNEL.getCode();
    }

    @Override
    public boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam) {
        System.out.println("公众号处理逻辑");
        return true;
    }
}

app注册渠道的后置处理器

/**
 * @Author linhao
 * @Date created in 8:16 上午 2021/9/3
 */
public class AppRegisterHandler extends AbstractRegisterHandler {

    @Override
    public int getSource() {
        return RegisterConstants.RegisterEnum.APP_CHANNEL.getCode();
    }

    @Override
    public boolean doPostProcessorAfterRegister(RegisterInputParam registerInputParam) {
        System.out.println("app处理逻辑");
        return true;
    }
}

不同的注册渠道号通过一个枚举来进行管理:

public class RegisterConstants {

    public enum RegisterEnum{

        GZH_CHANNEL(0,"公众号渠道"),
        APP_CHANNEL(1,"app渠道");

        RegisterEnum(int code, String desc) {
            this.code = code;
            this.desc = desc;
        }

        int code;
        String desc;

        public int getCode() {
            return code;
        }
    }
}

接下来,对于注册的后置处理服务接口进行实现:

/**
 * @Author linhao
 * @Date created in 7:48 上午 2021/9/3
 */
public class RegisterServiceImpl implements IRegisterService {

    private static List<AbstractRegisterHandler> registerHandlerList = new ArrayList<>();

    static {
        registerHandlerList.add(new GZHRegisterHandler());
        registerHandlerList.add(new AppRegisterHandler());
    }

    @Override
    public void postProcessorAfterRegister(RegisterInputParam registerInputParam) {
        for (AbstractRegisterHandler abstractRegisterHandler : registerHandlerList) {
            if(abstractRegisterHandler.getSource()==registerInputParam.getSource()){
                abstractRegisterHandler.doPostProcessorAfterRegister(registerInputParam);
                return;
            }
        }
        throw new RuntimeException("未知注册渠道号");
    }

}

最后通过简单的一段测试程序:

public class Test {

    public static void main(String[] args) {
        RegisterInputParam registerInputParam = new RegisterInputParam();
        registerInputParam.setUserId(10012);
        registerInputParam.setSource(0);

        IRegisterService registerService = new RegisterServiceImpl();
        registerService.postProcessorAfterRegister(registerInputParam);

        RegisterInputParam registerInputParam2 = new RegisterInputParam();
        registerInputParam2.setUserId(10013);
        registerInputParam2.setSource(1);
        registerService.postProcessorAfterRegister(registerInputParam2);

        System.out.println("=======");

    }
}

这样的设计和起初最先前的设计相比有几处不同的完善点:
新增不同注册渠道的时候,只需要关心注册渠道的source参数。
同时对于后续业务的拓展,新增不同的注册渠道的时候,RegisterServiceImpl只需要添加新编写的注册处理器类即可。

在这里插入图片描述

再回过头来看,这样的一段代码设计是否满足了开放封闭原则呢?
每次新增不同的注册类型处理逻辑之后,程序中都只需要新增一种Handler处理器,这种处理器对于原先的业务代码并没有过多的修改,从整体设计的角度来看,并没有对原有的代码结构造成影响,而且灵活度相比之前有所提高。这也正好对应了,对扩展开放,对修改关闭。

如果你对设计模式有一定了解的话,可能还会发现大多数常用的设计模式都在遵守这一项原则,例如模版模式,策略模式,责任链模式等等。

里氏替换原则

我认为,里氏替换原则更多是体现在了父子类继承方面,强调的是子类在继承了父类对象的时候不应该破坏这个父类对象的设计初衷。

举个例子来说:
我们定义了一个提款的服务:

/**
 * @Author linhao
 * @Date created in 11:21 上午 2021/9/4
 */
public interface DrawMoneyService {
    /**
     * 提款函数
     *
     * @param drawMoneyInputParam
     */
    void drawMoney(DrawMoneyInputParam drawMoneyInputParam);
}

对应的是一个抽象实现父类:

/**
 * @Author linhao
 * @Date created in 11:25 上午 2021/9/4
 */
public abstract class AbstractDrawMoneyServiceImpl implements DrawMoneyService{

    /**
     * 设计初衷,需要对提现金额进行参数校验
     * 
     * @param drawMoneyInputParam
     */
    @Override
    public abstract void drawMoney(DrawMoneyInputParam drawMoneyInputParam);
}

正常的子类继承对应父类都应该是对入参进行一个校验判断,如果金额数值小于0,自然就不允许提现了。

/**
 * @Author linhao
 * @Date created in 11:22 上午 2021/9/4
 */
public class AppDrawMoneyServiceImpl extends AbstractDrawMoneyServiceImpl{

    @Override
    public void drawMoney(DrawMoneyInputParam drawMoneyInputParam) {
        if(drawMoneyInputParam.getMoney()>0){
            //执行提款程序
        }
        System.out.println("app提款业务");
    }
}

但是如果某个实现的子类当中违背了这一设计原则,例如下边这种:

public class GZHDrawMoneyServiceImpl implements DrawMoneyService {
    @Override
    public void drawMoney(DrawMoneyInputParam drawMoneyInputParam) {
        if(drawMoneyInputParam.getMoney()<0){
            //执行提款程序
        }
        System.out.println("公众号提款业务");
    }
}

那么这种情况下,子类的实现就违背了最初父类设计的初衷,此时就违背了里氏替换原则的思想。此时就容易给阅读代码的人感觉,不同的子类虽然都继承了同一个父类,但是在转账的参数校验逻辑上完全是东一套,西一套,没有特定的规矩,逻辑比较乱。

所以较好的做法是在父类中就将需要满足的基本逻辑定义好,保证子类在进行扩展的时候不会轻易造成修改。

另外说说多态和里氏替换原则两个名词:

从案例代码来看,你会发现似乎 多态 和 里氏替换 长得很相似。但是我个人认为这是两个不同领域的东西,前者是代码特有的属性,后者则是一种设计思想,正因为类有了多态的这种特性,人们才会重视在代码设计过程中需要遵守里氏替换原则。这一项原则在设计的过程中保证了代码设计的正确性,它更像是一种思路在指导着开发者如何设计出更加好维护和理解的程序。

接口隔离原则

关于接口隔离原则这部分,我们可以通过一个具体的实战案例来学习。

在和第三方服务进行对接的时候,通常我们需要接入一些密钥之类的相关信息,例如和支付宝的支付接口对接,和微信支付接口做对接,和银联支付做对接等等。
那么我们可以将这些不同场景下关于支付相关的信息的储存放在一个Config相关的对象中,如下所示:

/**
 * 基本的支付配置接口
 * 
 * @Author linhao
 * @Date created in 9:59 上午 2021/9/5
 */
public interface BasePayConfig {
}

然后对每类支付配置都有对应的一个实现方式:

public class BankPayConfig implements BasePayConfig{
    private String secretKey;
    private String appId;
    private String randomNumber;
    //getter和setter省略
}

public class AliPayConfig implements BasePayConfig{
    private String secretKey;
    private String appId;
    private String randomNumber;
}

public class WXPayConfig implements BasePayConfig{
    private String secretKey;
    private String appId;
    private String randomNumber;
}

然后呢,实际场景中我们需要将这些配置信息给展示到一个后台管理系统的某个模块当中,所以后续我便在已有的BasePayConfig接口中定义了一个专门展示支付配置的函数:

public interface BasePayConfig {

    /**
     * 展示配置
     */
    Map<String,Object> showConfig();
}

展示配置之后,需要在各个子类中去对不同的信息进行组装,最后返回一个Map的格式给到调用方。
但是随着业务的变动,某天需要对微信支付的配置信息实现可以替换更新的功能,但是额外的支付宝支付,银联支付不允许对外暴露这一权限。那么此时就需要对代码进行调整了。

调整思路一:
直接在BasePayConfig接口中进行扩展,代码案例如下:

public interface BasePayConfig {

    /**
     * 展示配置
     */
    Map<String,Object> showConfig(int code);
    
    /**
     * 更新配置信息
     * 
     * @return
     */
    Map<String,Object> updateConfig();
}

然后各个子类依旧是实现这些接口,并且即使不需要实现更新功能的支付宝配置类,银联配置类都必须强制实现。从这样的设计角度来思考就会发现,对于代码实现方面不是太友好,接口内部定义的函数粒度还可以再分细一些。

调整思路二:
将读取配置和更新配置分成两个接口,需要实现更新配置功能的类才需要去实现该接口。代码如下所示:
支付配置展示

/**
 * @Author linhao
 * @Date created in 10:19 上午 2021/9/5
 */
public interface BasePayConfigViewer {
    /**
     * 展示配置
     */
    Map<String,Object> showConfig(int code);
}

支付配置更新

public interface BasePayConfigUpdater {

    /**
     * 更新配置信息
     *
     * @return
     */
    Map<String,Object> updateConfig();
}

这样的设计能够保证,不同的接口专门负责不同的领域,只有当实现类确实需要使用该功能的时候才去实现该接口。写到这里的时候,你可以不妨再回过头去理解下我在文章上半部分中提及的接口隔离原则,相信你会有新的体会。

或许你也会有所疑惑,接口隔离原则好像和单一责任原则有些类似呀,都是各自专一地负责自己所管理的部分。但是我个人认为,接口隔离原则关注的是接口,而单一责任原则关注的目标可以是对象,接口,类,所涉及的领域更加广阔一些。

依赖反转原则

在介绍依赖反转原则之前,我们先来理解一个相似的名词,控制反转。

单纯的从Java程序来进行理解:
例如我们定义个BeanObject对象:

public interface BeanObject {
    void run();
}

然后再定义相关的实现类,如消息发送:

public class MessageNotify implements BeanObject{

    @Override
    public void run() {
        System.out.println("消息发送");
    }
}

最后是一个Context上下文环境:

public class BeanContext {

    private static List<BeanObject> beanObjectList = new ArrayList<>();

    static {
        beanObjectList.add(new MessageNotify());
    }

    public static void main(String[] args) {
        beanObjectList.get(0).run();
    }
}

从代码来看,可以发现对于MessageNotify的调用均是通过一个BeanContext组件调用来实现的,而并不是直接通过new MessageNotify的方式去显示调用。通过封装一个基础骨架容器BeanContext来管控每个BeanObject的run方法执行,这样就将该函数的调用权转交给了BeanContext对象管理。

控制反转
现在我们再来理解 控制反转 这个名词,“控制”主要是指对程序执行流程的控制,例如bean的调用方式。“反转”则是指程序调用权限的转变,例如从bean的调用方转变为了基础容器。

依赖注入
再来聊下依赖注入这个名词。
依赖注入强调的是将依赖属性不要通过显式的new方式来创建注入,而是将其交给了基础框架去管理。这方面的代表框架除了我们熟悉的Spring之外,其实还有很多,例如Pico Contanier等。

最后再来品味下官方对于依赖反转的介绍:

High-level modules shouldn’t depend on low-level modules. Both
modules should depend on abstractions. In addition, abstractions
shouldn’t depend on details. Details depend on abstractions.

高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。

最后,希望这篇文章能够对你有所启发。

<think>好的,我现在需要帮用户详细聊聊评估指标的设计,特别是在AI产品经理算法团队在预训练模型微调阶段的合作中的角色。用户之前已经问过AI产品经理在微调阶段的作用如何与算法团队配合,现在想更深入地了解评估指标的设计部分。 首先,我需要回顾之前的对话,确保这次回答连贯。之前我提到了评估指标设计是产品经理的核心作用之一,需要与算法团队共同制定符合业务目标的评估体系。现在用户希望更详细地了解这一部分,可能需要分步骤说明,涵盖评估指标的类型、设计流程、注意事项以及案例分析。 用户可能的深层需求是希望了解如何具体操作,比如如何选择适合的指标,如何避免常见陷阱,或者如何在实际项目中应用这些指标。因此,我需要结构清晰,分点阐述,可能包括指标的分类(通用指标、业务指标、人工评估)、设计步骤、设计原则、常见误区、案例分析等部分。 需要注意使用用户之前提到的格式要求:行内数学公式用$...$,独立公式用$$...$$,但用户这次的问题可能不需要太多公式,不过如果有的话需要正确使用LaTeX。比如在提到准确率、F1值等时,可能需要公式,比如准确率是$\frac{TP+TN}{TP+TN+FP+FN}$,F1是$\frac{2 \times Precision \times Recall}{Precision + Recall}$等。 还需要考虑用户可能是AI产品经理或相关角色,希望了解实际操作中的评估指标设计,所以内容需要实用,结合案例,避免过于理论化。同时,可能需要提醒产品经理在指标设计时与算法团队沟通的重要性,以及如何平衡不同指标之间的冲突,比如准确率与响应时间的权衡。 可能的误区包括过度依赖单一指标、忽略业务场景的特殊性、未考虑数据分布等,这些都需要在回答中提及。此外,案例分析部分可以举一个具体的例子,比如在客服系统中如何设计指标,或者在内容审核中的多维度评估。 最后,总结部分需要强调评估指标设计的重要性,以及产品经理在其中起到的桥梁作用,确保技术指标与业务目标一致。同时,可以建议定期复审指标,根据业务变化调整评估体系。</think>以下是关于评估指标设计的详细解析,结合业务场景与技术实现,分步骤说明其核心逻辑及实操方法: --- ### **一、评估指标的核心价值** 评估指标是衡量模型效果的“尺子”,需同时满足两个目标: 1. **技术可量化**:能被算法团队用于优化模型(如损失函数、准确率)。 2. **业务可感知**:能直接反映业务效果(如用户留存率、审核通过率)。 --- ### **二、评估指标的分类与设计方法** #### **1. 通用技术指标(算法团队主导)** 用于基础模型性能评估,需与产品经理对齐业务权重: - **分类任务** - 准确率:$\text{Accuracy} = \frac{TP+TN}{TP+TN+FP+FN}$(适用于类别均衡场景) - F1值:$\text{F1} = \frac{2 \times \text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}}$(平衡精确率与召回率) - AUC-ROC曲线:衡量排序能力,适合点击率预测等场景。 - **生成任务** - BLEU:基于n-gram匹配,常用在机器翻译(需注意对长文本的局限性)。 - ROUGE:关注召回率,适合文本摘要评估。 - 人工评估:通过众包标注判断生成内容的流畅性、相关性。 - **其他任务** - 目标检测:mAP(mean Average Precision) - 推荐系统:NDCG(归一化折损累积增益) **设计要点**: - 明确指标缺陷(如BLEU无法评估语义正确性) - 多指标组合(例:F1值+人工审核通过率) #### **2. 业务定制指标(产品经理主导)** 需将业务目标转化为可量化公式: - **客服对话系统** - 问题解决率 = $\frac{\text{用户未转人工的会话数}}{\text{总会话数}}$ - 平均对话轮次:反映效率(值过低可能说明模型未理解用户意图) - **内容审核模型** - 漏杀率 = $\frac{\text{未识别违规内容数}}{\text{总违规内容数}}$ - 误杀率 = $\frac{\text{误判正常内容数}}{\text{总正常内容数}}$ - **平衡策略**:设置阈值使漏杀率≤1%且误杀率≤5%(需与风控团队协商) - **电商推荐模型** - 购买转化率 = $\frac{\text{点击后购买的用户数}}{\text{总点击用户数}}$ - 长尾商品覆盖率 = $\frac{\text{被推荐的长尾商品SKU数}}{\text{总长尾商品SKU数}}$ **设计要点**: - 定义指标时需明确统计口径(如“长尾商品”的具体定义) - 区分核心指标与观察指标(例:核心指标是漏杀率,观察指标是响应延迟) #### **3. 人工评估体系** 当自动指标无法覆盖复杂场景时使用: - **评分维度设计**(以生成任务为例): ```markdown | 维度 | 评分标准 | 权重 | |------------|-----------------------------------|------| | 相关性 | 回答是否与问题直接相关(1-5分) | 40% | | 事实准确性 | 是否存在事实错误(1-5分) | 30% | | 可读性 | 语句是否通顺、符合语法(1-3分) | 20% | | 价值观安全 | 是否包含敏感/偏见内容(一票否决) | 10% | ``` - **操作规范**: - 提供标注手册(例:如何界定“中性评论”) - 使用Kappa系数检验标注一致性(通常要求>0.6) --- ### **三、评估指标设计流程** 1. **需求拆解** - 与业务方确认核心目标(例:风控场景“宁可错杀不可漏杀”) - 识别约束条件(如模型延迟必须≤300ms) 2. **指标候选集构建** - 技术指标:从算法论文/实践中选择(如目标检测用mAP) - 业务指标:通过用户调研、历史数据分析提取(例:发现客服对话中70%的用户问题集中在5类意图) 3. **指标权重分配** - 使用层次分析法(AHP)量化优先级: - 业务专家对指标两两比较重要性 - 计算权重向量(需通过一致性检验) 4. **基线设定与迭代** - 基于当前业务表现设定基线(例:现有规则引擎漏杀率3%,目标微调后降至1%) - 动态调整:当业务场景变化(如增商品品类)时重校准指标 --- ### **四、常见误区与解决方案** | 误区类型 | 典型案例 | 解决方案 | |-------------------------|-----------------------------------|-------------------------------| | **指标与业务目标脱节** | 优化翻译模型的BLEU值,但实际用户更关注术语准确性 | 增术语准确率专项评估 | | **单一指标过拟合** | 为提升F1值过度调整阈值,导致用户体验下降 | 入人工评估或满意度调查 | | **忽略数据分布影响** | 在类别不均衡数据中使用准确率(如欺诈检测) | 改用F1值或AUC-ROC | | **评估场景覆盖不全** | 只测试高频case,未覆盖边缘场景(如方言识别) | 构建包含长尾case的测试集 | --- ### **五、典型案例分析** #### **案例:智能客服意图识别微调** - **业务需求**:减少转人工率(当前30% → 目标15%) - **指标设计过程**: 1. 拆解技术指标: - 意图分类准确率(F1值) - 未知意图检测召回率(识别用户问题是否超出预设范围) 2. 定制业务指标: - 转人工率 = $\frac{\text{主动请求人工的会话数}}{\text{总会话数}}$ - 问题解决率(通过用户反馈按钮统计) 3. 人工评估规则: - 抽样检查模型是否将“我要退订套餐”错误归类为“查询套餐” --- ### **六、协作关键点** 1. **指标透明化**:向算法团队解释为何选择特定指标(例:为何不用准确率而用F1值) 2. **工具共建**:开发自动化评估脚本(如定期运行测试集生成指标报告) 3. **冲突调解**:当技术指标提升但业务指标下降时,推动根因分析(如模型过度压缩导致效果损失) 通过科学设计评估指标,AI产品经理能有效引导算法优化方向,避免“模型表现好但业务价值低”的常见困局。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值