2023年4月

PS:要转载请注明出处,本人版权所有。

PS: 这个只是基于《我自己》的理解,

如果和你的原则及想法相冲突,请谅解,勿喷。

环境说明

Ubuntu 18.04.x

前言


近一年来,虽然还是做的是AIOT相关的事情,但是某些事情却发生了一些变化。随着个人的阅历提升,现在的AI在边缘端部署已经不局限于传统的linux这样的形态,这一年来,我已经逐渐接触到android的边缘端盒子这样的概念了。

对于Android来说,我之前有所了解,但是停留的非常表面,只知道其是一个Linux内核+Android Runtime+app的这样的形态。但是如果我们将Android Runtime和App看作普通的Linux app,那么我们会发现Android和传统Linux的差别没有那么大,我们甚至可以将Android当成一个Linux发行版来使用。但是实际在使用过程中,最大的差异在于Android引入了许多的Android特有的内容,例如binder,log,adb等等,其次和linux下面编程的最大区别还是在于他们的基础c库不一样,一个是bonic c,一个的glibc,这一点可以说是贯穿我在使用Android的整个过程中。

在使用Android的过程中,我们会听见一个HAL的词,整个HAL可以说是Android能够成功商业化的一个重要因素,因为其可以保护各个厂商的利益,然后反过来,正是由于各个厂家的支持,导致了Android的生态是非常丰富的。那么我们来看看这个HAL到底是干嘛的。

由于网上有许多介绍HAL的文章了,本文不会重复一些基础的内容,因此本文后续的阅读需要读者至少对Linux和Android HAL有一个基础的了解后,才建议阅读本文。





HAL 深入分析


首先Android HAL分为大概分为两个版本,一个新的和旧的,本文重点分析新版HAL原理。其中两种版本架构大概简介如下:

  1. 旧的HAL架构(libhardware_legacy.so)每个app都会加载模块,有重入问题,由于每个app直接加载对应的so,也导致app和模块接口耦合较大,非常不方便维护。
  2. 新的HAL架构module/stub,app访问对应硬件对应的服务,然后对应硬件服务通过抽象api,module id,设备id,代理后访问硬件。

上面对新的架构说的还是有些表面了,下面我们深入分析其中的重要的几个结构(在hardware.h中),然后最后通过一个实际例子来深入理解它。



struct hw_module_t

它的实际源码定义如下:

/**
 * Every hardware module must have a data structure named HAL_MODULE_INFO_SYM
 * and the fields of this data structure must begin with hw_module_t
 * followed by module specific information.
 */
typedef struct hw_module_t {
    /** tag must be initialized to HARDWARE_MODULE_TAG */
    uint32_t tag;

    /**
     * The API version of the implemented module. The module owner is
     * responsible for updating the version when a module interface has
     * changed.
     *
     * The derived modules such as gralloc and audio own and manage this field.
     * The module user must interpret the version field to decide whether or
     * not to inter-operate with the supplied module implementation.
     * For example, SurfaceFlinger is responsible for making sure that
     * it knows how to manage different versions of the gralloc-module API,
     * and AudioFlinger must know how to do the same for audio-module API.
     *
     * The module API version should include a major and a minor component.
     * For example, version 1.0 could be represented as 0x0100. This format
     * implies that versions 0x0100-0x01ff are all API-compatible.
     *
     * In the future, libhardware will expose a hw_get_module_version()
     * (or equivalent) function that will take minimum/maximum supported
     * versions as arguments and would be able to reject modules with
     * versions outside of the supplied range.
     */
    uint16_t module_api_version;
#define version_major module_api_version
    /**
     * version_major/version_minor defines are supplied here for temporary
     * source code compatibility. They will be removed in the next version.
     * ALL clients must convert to the new version format.
     */

    /**
     * The API version of the HAL module interface. This is meant to
     * version the hw_module_t, hw_module_methods_t, and hw_device_t
     * structures and definitions.
     *
     * The HAL interface owns this field. Module users/implementations
     * must NOT rely on this value for version information.
     *
     * Presently, 0 is the only valid value.
     */
    uint16_t hal_api_version;
#define version_minor hal_api_version

    /** Identifier of module */
    const char *id;

    /** Name of this module */
    const char *name;

    /** Author/owner/implementor of the module */
    const char *author;

    /** Modules methods */
    struct hw_module_methods_t* methods;

    /** module's dso */
    void* dso;

#ifdef __LP64__
    uint64_t reserved[32-7];
#else
    /** padding to 128 bytes, reserved for future use */
    uint32_t reserved[32-7];
#endif

} hw_module_t;

一个hw_module_t代表一个硬件模块,但是一个硬件模块可能包含了很多的硬件设备,所以我们要操作一个实际的硬件设备,按照这套框架,第一件事获取模块,第二件事就是打开设备,第三操作设备,第四关闭设备。

其实这里的注释说的很清楚,使用它,有两个注意事项,一是必须要在实际模块中定义一个HAL_MODULE_INFO_SYM的结构体变量,且此结构体必须是struct hw_module_t 作为第一个成员变量。这里的根本原因是因为c的结构体内存布局和暴露这个结构体的名字,后面会详细说这个事情。



struct hw_module_methods_t

它的实际源码定义如下:

typedef struct hw_module_methods_t {
    /** Open a specific device */
    int (*open)(const struct hw_module_t* module, const char* id,
            struct hw_device_t** device);

} hw_module_methods_t;

此结构体没啥可说的,就是通过实际的模块,然后传入一个硬件设备的id,然后打开实际的硬件设备。因此在每个实际的hw_module_t中,都包含了一个hw_module_methods_t,然后有打开设备的操作。这里也体现出来了一个模块可以有多个设备的这种概念。



struct hw_device_t

它的实际源码定义如下:

/**
 * Every device data structure must begin with hw_device_t
 * followed by module specific public methods and attributes.
 */
typedef struct hw_device_t {
    /** tag must be initialized to HARDWARE_DEVICE_TAG */
    uint32_t tag;

    /**
     * Version of the module-specific device API. This value is used by
     * the derived-module user to manage different device implementations.
     *
     * The module user is responsible for checking the module_api_version
     * and device version fields to ensure that the user is capable of
     * communicating with the specific module implementation.
     *
     * One module can support multiple devices with different versions. This
     * can be useful when a device interface changes in an incompatible way
     * but it is still necessary to support older implementations at the same
     * time. One such example is the Camera 2.0 API.
     *
     * This field is interpreted by the module user and is ignored by the
     * HAL interface itself.
     */
    uint32_t version;

    /** reference to the module this device belongs to */
    struct hw_module_t* module;

    /** padding reserved for future use */
#ifdef __LP64__
    uint64_t reserved[12];
#else
    uint32_t reserved[12];
#endif

    /** Close this device */
    int (*close)(struct hw_device_t* device);

} hw_device_t;

此结构体就是上文我们说的实际打开的设备结构体,一般情况我们会将此结构体暴露到对应hal的头文件中,因为这个包含了实际操作硬件的一些接口信息。注意这个hw_device_t包含了一个close接口,是每个设备的关闭接口。

注意,我们这个时候没有去说hardware.c所做的事情,也就是如下两个接口到底做了什么,这个问题的解答,我们留到下一小节例子中去深入认识他。

/**
 * Get the module info associated with a module by id.
 *
 * @return: 0 == success, <0 == error and *module == NULL
 */
int hw_get_module(const char *id, const struct hw_module_t **module);

/**
 * Get the module info associated with a module instance by class 'class_id'
 * and instance 'inst'.
 *
 * Some modules types necessitate multiple instances. For example audio supports
 * multiple concurrent interfaces and thus 'audio' is the module class
 * and 'primary' or 'a2dp' are module interfaces. This implies that the files
 * providing these modules would be named audio.primary.<variant>.so and
 * audio.a2dp.<variant>.so
 *
 * @return: 0 == success, <0 == error and *module == NULL
 */
int hw_get_module_by_class(const char *class_id, const char *inst,
                           const struct hw_module_t **module);





一个MY_HW的硬件模块的HAL例子


如上,我们已经介绍了hal里面的重要的3个结构体,但是如果就到此的话,其实我们对hal还是一知半解,这个时候,我们可以尝试自己虚拟一个硬件出来,然后设计HAL接口。这样可以实际体会HAL的工作原理。

首先,我们定义我们的模块叫做MY_HW。



下面是自定义的hal模块源码

my_hw_hal.h 源码

#ifndef __MY_HW_HAL_H__
#define __MY_HW_HAL_H__


#include "hardware.h"

#define MY_HW_MODULE_ID "MY_HW"


struct my_hw_device_t{

    struct hw_device_t base;

    int (*set_my_hw_op)(struct my_hw_device_t * dev, int op_type);
};

#endif //__MY_HW_HAL_H__

my_hw_hal.c 源码

#include "my_hw_hal.h"

#include <stdio.h>
#include <string.h>
int my_hw_open(const struct hw_module_t* module, const char* id,
            struct hw_device_t** device);
int set_my_hw_op_device_0 (struct my_hw_device_t * dev, int op_type);
int my_hw_close_device_0(struct hw_device_t* device);

//For hw_moudle_methods_t
static struct hw_module_methods_t my_hw_methods = {

    .open = my_hw_open,
};



//For hw_module_t
struct my_hw_module_t {

    struct hw_module_t base;
};




__attribute__((visibility("default")))  struct  my_hw_module_t  HAL_MODULE_INFO_SYM = {

    .base = {

        .tag = HARDWARE_MODULE_TAG,
        .module_api_version = 0,
        .hal_api_version = 0,
        .id = MY_HW_MODULE_ID,
        .name = "MY HW MODULE",
        .author = "Sky",
        .methods = &my_hw_methods
    }
};

//For hw_device_t
static struct my_hw_device_t my_hw_device_0 = {
    .base = {
        .tag = HARDWARE_DEVICE_TAG,
        .version = 0,
        .module = (hw_module_t*)&HAL_MODULE_INFO_SYM,
        .close = my_hw_close_device_0
    },
    .set_my_hw_op = set_my_hw_op_device_0
};



//For hw_device_t
int set_my_hw_op_device_0 (struct my_hw_device_t * dev, int op_type)
{
    printf("set_my_hw_op_device_0() op_type = %d\n", op_type);
    return 0;
}

int my_hw_close_device_0(struct hw_device_t* device)
{
    printf("my_hw_close_device_0()\n");
    return 0;
}




//For hw_moudle_methods_t
int my_hw_open(const struct hw_module_t* module, const char* id,
            struct hw_device_t** device)
{
    printf("my_hw_open() device id %s\n", id);
    if (strcmp(id, "0") != 0){
        printf("my_hw_open() failed\n");
        return -1;
    }
    *device = (struct hw_device_t*)&my_hw_device_0;

    return 0;
}

从这里我们可以看出,新模块的实现就是对3个结构体的继承和实现。同时对一些成员变量进行赋值。



MY_HW模块源码的分析
__attribute__((visibility("default")))  struct  my_hw_module_t  HAL_MODULE_INFO_SYM = {

    .base = {

        .tag = HARDWARE_MODULE_TAG,
        .module_api_version = 0,
        .hal_api_version = 0,
        .id = MY_HW_MODULE_ID,
        .name = "MY HW MODULE",
        .author = "Sky",
        .methods = &my_hw_methods
    }
};

my_hw_module_t 的定义,就是定义一个HAL_MODULE_INFO_SYM(它是一个宏定义)变量,注意此宏定义会被替换为一个HMI的名字,此名字是所有HAL模块必须暴露的一个符号。且必须叫做这个名字,因为这是libhardware.so中读取它的约定。

此外,hw_module_t必须在我定义的变量的开始位置,这样方便类型转换。

//For hw_moudle_methods_t
static struct hw_module_methods_t my_hw_methods = {

    .open = my_hw_open,
};

hw_module_methods_t 的定义,实现一个真正的设备打开接口。

//For hw_device_t
static struct my_hw_device_t my_hw_device_0 = {
    .base = {
        .tag = HARDWARE_DEVICE_TAG,
        .version = 0,
        .module = (hw_module_t*)&HAL_MODULE_INFO_SYM,
        .close = my_hw_close_device_0
    },
    .set_my_hw_op = set_my_hw_op_device_0
};

my_hw_device_t的定义,此设备就是我们这个模块定义的一个设备,此设备通过my_hw_module_t中的open接口打开,然后提供相关的接口给HAL相关的程序使用。



综合分析

这里我们简单设计一个服务程序来调用我们封装的hal模块,其流程就是调用hw_get_module获取实际module地址,然后通过module打开对应设备,然后操作设备,最后关闭设备。

#include "my_hw_hal.h"

int main(int argc, char * argv[])
{
    hw_module_t * hwmodule = nullptr;
    my_hw_device_t * my_hw_device = nullptr;

    int _ret = hw_get_module(MY_HW_MODULE_ID, (const hw_module_t**)&hwmodule);

    #define MY_HW_DEVICE_ID "0"
    _ret = hwmodule->methods->open(hwmodule, MY_HW_DEVICE_ID, (hw_device_t**)&my_hw_device);

    #define MY_HW_DEVICE_ID_0_OP_TYPE_0 0
    my_hw_device->set_my_hw_op(my_hw_device, MY_HW_DEVICE_ID_0_OP_TYPE_0);

    my_hw_device->base.close((hw_device_t*)my_hw_device);
    return 0;
}

然后通过如下编译脚本生成两个so和一个应用程序。

#!/bin/bash


# for hardware so
gcc -fPIC -c hardware.c -I . -fvisibility=hidden
gcc -shared hardware.o -o libhardware.so -ldl -fvisibility=hidden
strip libhardware.so



# for MY_HW.sky-sdk.so
gcc -fPIC -c my_hw_hal.c -I . -fvisibility=hidden
gcc -shared my_hw_hal.o -o MY_HW.sky-sdk.so -fvisibility=hidden
strip MY_HW.sky-sdk.so


# for my hw service 
g++ my_hw_service.cpp -o my_hw_service -L . -l hardware -I .  -fvisibility=hidden

我们先来看看我们应用程序执行的结果如下:

rep_img

我们可以看到,第一步通过hw_get_module获取到一个模块信息,这里其实在hardware.c里面定义的很清楚,直接通过dlopen/dlsym 一个HMI的符号得到了我们定义的my_hw_module_t的变量地址,由于c的内存布局的原因,本来这个地址存放的是my_hw_module_t变量,但是可以直接强转为hw_module_t变量。简单来说,这就是一种c里面实现类似c++继承的方法,由于内存布局是连续的,根据hw_module_t的大小,可以直接从my_hw_module_t前面部分转换为hw_module_t。这也是hw_module_t必须放在my_hw_module_t中开始的原因。

我们也可知道,在hardware.c中,hw_get_module是根据id来在特定目录中去搜索相关的模块so,然后通过dlopen打开它并进行后续的操作。如我修改的hardware.c部分节选:

rep_img

同理,到了这里,我们不用猜测,一定在MY_HW.sky-sdk.so暴露了一个HMI的符号。如图:

rep_img

注意hardware.c和hardware.h直接从android源码中拿出来,简单做修改即可在linux里面编译。这里我们简单看看libhardware.so的符号暴露信息:

rep_img

这里其实就暴露了上面提到的两个接口,hw_get_module和hw_get_module_by_class。





后记


总的来说:

  • hw_module_methods_t 可以用来标识模块的公用方法,当前具备了一个open方法,注意一个module对应多个设备功能。
  • hw_get_module() 主要是使用传入的id,然后通过id和一些属性通过dlopen加载so。注意'HMI'这个符号,这个符号是存放的hw_module_t作为基类的地址。通过此地址可以打开这个模块中的特有设备,并提供特定操作。
  • hw_module_t 可以用来标识一个模块,首先通过hw_get_module()获取当前hw_module_t,然后通过当前hw_module_t的hw_module_methods_t中的open方法打开设备。
  • hw_device_t 可以用来标识模块下的一个设备,其中的close方法用来关闭本设备。

注意三个结构体之间的关系:hw_get_module()获取hw_module_t,hw_module_t通过hw_module_methods_t获取hw_device_t,hw_device_t中携带了当前设备的各种操作方法,其实HAL的另一个重要部分是在hw_device_t中定义当前设备的通用接口。

其实HAL的整个原理并不复杂,在Linux内核源码中,你会看到大量的类似的操作。归根到底,其实HAL的这种封装,就是一种应用技巧。

参考文献




打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
qrc_img

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。

Java之SPI机制详解

1: SPI机制简介

SPI
全称是
Service Provider Interface
,是一种
JDK
内置的动态加载实现扩展点的机制,通过
SPI
技术我们可以动态获取接口的实现类,不用自己来创建。这个不是什么特别的技术,只是 一种
设计理念

2: SPI原理

image

Java SPI 实际上是
基于接口的编程+策略模式+配置文件
组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是
解耦

3: 使用场景

调用者根据实际使用需要
启用、扩展、或者替换框架的实现策略

下面是一些使用了该机制的场景

  • JDBC驱动,加载不同数据库的驱动类
  • Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
  • Tomcat 加载 META-INF/services下找需要加载的类
  • SpringBoot项目中 使用@SpringBootApplication注解时,会开始自动配置,而启动配置则会去扫描META-INF/spring.factories下的配置类

4: 源码论证

4.1 应用程序调用ServiceLoader.load方法
ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量

    private static final String PREFIX = "META-INF/services/";


  private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

	/** 
     * 
     * 在调用该方法之后,迭代器方法的后续调用将延迟地从头开始查找和实例化提供程序,就像新创建的加载程序所做的		  那样
     */
   public void reload() {
        providers.clear(); //清除此加载程序的提供程序缓存,以便重新加载所有提供程序。
        lookupIterator = new LazyIterator(service, loader);
    }

	private class LazyIterator implements Iterator<S>{

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;


        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //找到配置文件
                    String fullName = PREFIX + service.getName();
                    //加载配置文件中的内容
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                //解析配置文件
                pending = parse(service, configs.nextElement());
            }
            //获取配置文件中内容
            nextName = pending.next();
            return true;
        }
    }

		/** 
     	* 
     	*  通过反射 实例化配置文件中的具体实现类
    	 */
		private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

5: 实战

步骤1
新建以下类

public interface IService {

    /**
     * 获取价格
     * @return
     */
    String getPrice();

    /**
     * 获取规格信息
     * @return
     */
    String getSpecifications();
}
public class GoodServiceImpl implements IService {

    @Override
    public String getPrice() {
        return "2000.00元";
    }

    @Override
    public String getSpecifications() {
        return "200g/件";
    }
}

public class MedicalServiceImpl implements IService {

    @Override
    public String getPrice() {
        return "3022.12元";
    }

    @Override
    public String getSpecifications() {
        return "30粒/盒";
    }
}

步骤2
、在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 org.example.IService.txt 。内容是要应用的实现类,我这边需要放入的数据如下

org.example.GoodServiceImpl
org.example.MedicalServiceImpl

步骤3
、使用 ServiceLoader 来加载配置文件中指定的实现。

public class Main {
    public static void main(String[] args) {
        final ServiceLoader<IService> serviceLoader = ServiceLoader.load(IService.class);
        serviceLoader.forEach(service -> {
            System.out.println(service.getPrice() + "=" + service.getSpecifications());
        });
    }
}

输出:

2000.00元=200g/件
3022.12元=30粒/盒

6: 优缺点

6.1 优点

解耦
使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起,应用程序可以根据实际业务情况启用框架扩展或替换框架组件。相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架,不必关心接口的实现类的路径

6.2
缺点

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类
  • 多个并发多线程使用ServiceLoader类的实例是不安全的


机器学习是一种人工智能的分支,它使用算法和数学模型来让计算机自主学习数据并做出预测和决策。这种技术正在被广泛应用于各种领域,包括自然语言处理、计算机视觉、语音识别、医学诊断和金融预测等。在本篇博客中,我们将介绍机器学习的基本概念、算法和应用,并提供一些代码和分析。

机器学习基本概念

机器学习是一种基于数据的算法,通过对大量数据进行学习,发现数据的规律和模式,并将这些规律应用到新的数据上,从而做出预测和决策。与传统的计算机程序不同,机器学习算法不需要人工编写所有的规则和逻辑,而是能够自主地从数据中学习并做出预测。机器学习的核心是构建模型,并利用训练数据对模型进行优化。训练数据通常包括输入数据和对应的输出数据,例如图像识别中的图片和图片中所表示的物体。机器学习的任务通常可以分为分类、回归、聚类等。

  • 分类任务:给定一个输入数据,将其分为多个类别中的一种,例如图像识别中将图片识别为猫或狗。
  • 回归任务:给定一个输入数据,预测其输出值,例如房价预测中,根据房屋的面积、位置等信息,预测该房屋的售价。
  • 聚类任务:将输入数据分为多个类别,使得同一类别内的数据相似度高,不同类别之间相似度低,例如利用用户的购买记录将用户进行分类,从而实现个性化推荐。

机器学习算法类型

机器学习算法主要分为三种类型:监督学习、无监督学习和强化学习。

  1. 监督学习

监督学习是一种使用带标签数据进行训练的机器学习算法。在监督学习中,我们给定一组输入数据和对应的输出结果,算法通过学习输入和输出之间的关系来建立一个预测模型。当给定新的输入数据时,模型可以预测相应的输出。监督学习算法包括线性回归、逻辑回归、决策树、支持向量机等。

  1. 无监督学习

无监督学习是一种使用不带标签数据进行训练的机器学习算法。在无监督学习中,我们给定一组输入数据,算法通过学习输入之间的关系来建立一个模型。无监督学习算法包括聚类、降维、关联规则等。

  1. 强化学习

强化学习是一种通过学习与环境交互的方式来优化行为的机器学习算法。在强化学习中,算法在与环境交互的过程中收到奖励或惩罚,通过学习如何最大化奖励来优化行为。强化学习算法包括Q学习、策略梯度等。

机器学习的实现步骤

  1. 数据预处理:包括数据清洗、特征提取等。
  2. 模型选择:选择适合任务的算法和模型,例如线性回归、决策树、支持向量机等。
  3. 模型训练:使用训练数据来训练模型,优化模型参数。
  4. 模型评估:使用测试数据来评估模型的性能。
  5. 模型应用:使用训练好的模型进行预测。

机器学习三个基本要素

机器学习通常包括三个基本要素:数据、模型和算法。数据是指用来训练和测试模型的样本数据,它通常包括输入和输出数据。模型是指用来描述数据之间关系的数学模型,它可以是线性模型、非线性模型、神经网络等。算法是指用来训练和优化模型的算法,常见的算法有梯度下降、支持向量机、决策树等。

机器学习相关应用

1.语音识别

语音识别是一种将语音信号转换成文字的技术。它是一种有监督学习,通常使用深度学习算法进行训练。语音识别的应用非常广泛,如智能语音助手、语音搜索、语音翻译等。

我们可以通过Python中的SpeechRecognition库来实现简单的语音识别。代码如下:

python import speech_recognition as sr

r = sr.Recognizer()
with sr.Microphone() as source:
    print("请开始说话...")
    audio = r.listen(source)
try:
    print("你说的是:" + r.recognize_google(audio, language='zh-CN'))
except sr.UnknownValueError:
    print("语音识别失败")
except sr.RequestError as e:
    print("网络异常:" + e)

该代码首先调用麦克风来录制音频,然后通过Google的语音识别API将音频转换成文字。该代码可以用于简单的语音识别应用,如命令识别、简单对话等。

2.图像识别

图像识别是一种将图像中的物体、场景、文字等信息识别出来的技术。它是一种有监督学习,通常使用深度学习算法进行训练。图像识别的应用非常广泛,如人脸识别、车牌识别、安防监控等。

我们可以通过Python中的OpenCV库来实现简单的图像识别。代码如下:

python import cv2

face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
cap = cv2.VideoCapture(0)
while True:
    ret, img = cap.read()
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    faces = face_cascade.detectMultiScale(gray, 1.3, 5)
    for (x, y, w, h) in faces:
        cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
    cv2.imshow('img', img)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

该代码使用OpenCV库来检测摄像头中的人脸。它使用Haar特征级联分类器来检测人脸,并在图像中标记出来。该代码可以用于简单的人脸识别应用,如安防监控、人脸认证等。

概述

本文向整个软件行业展示了出现GPT后的软件开发流程的颠覆性变化。由于这只是一个简单的案例,并没有涉及代码初次编写后的debug以及变更维护的流程。通过纳入GPT以及一些其他的开发环节和工具,后GPT时代的软件开发估计至少可以降低0%以上的人工编码量,50%以上的测试工作量,以数量级规模提成文档完整率。想进一步了解如何构造完整后GPT时代的开发流程请直接联系本人

文档都说重要,但几乎没人写

面试的时候,如果有应聘的人敢于掷地有声得说其实写代码不需要文档,那么大概率是不会被发offer的。但是在团队里如果有人每天坚持先写文档然后去写代码,或者每次改bug时候都要把相关文档都写清楚,估计这兄弟的KPI是上不去的。

虚伪的软件行业,并不是每个程序员都是渣男。但是造成这个结果一定是有深层次的行业问题在里面的,这是一个长达几十年没有被解决的问题。这个问题是阻碍软件开发质量和效率的一个核心问题。先设计后施工,谋定而后动,这本来就是除了软件编码行业以外的所有行业的常识。软件行业做为一个数字化程度比较落后的行业,距离信息化的差距是很明显的。

体验写文档的直观感受

今天有了GPT,大家都体会到了有超级助理的快感,所以我们可以先来体验一下有了GPT后的代码开发模式会变成什么样子

1.png

2.png

3.png

4.png

5.png

6.png

7.png

8.png

9.png

总结

只会编码的码农会在未来3-5年内大量失业。程序员的8小时内,编码的时间将会大大下降,而写文档的时间将会大大提升,一个全新的软件开发流程将会被建立。传统的设计、开发、测试的迭代流程会被大幅度的改变。希望中国软件行业能从今天开始就认真面对这个变革,不要在3-5年后再次让国家拨款成立专项基金后才开始追赶这个热点。


作者:BA2Ops
链接:https://juejin.cn/post/7219481966243430437
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

论文信息

论文标题:Semi-Supervised Domain Adaptation by Similarity based Pseudo-label Injection
论文作者:
Abhay Rawat
,
Isha Dua
,
Saurav Gupta
,
Rahul Tallamraju


论文来源:Published in ECCV Workshops 5 September 2022

论文地址:
download
论文代码:
download
视屏讲解:click

1 摘要

挑战:半监督域适应 (SSDA) 的主要挑战之一是标记的源样本和目标样本数量之间的比例偏差,导致模型偏向源域;

问题:SSDA 最近的工作表明,仅将标记的目标样本与源样本对齐可能会导致目标域与源域的域对齐不完整;

2 介绍

无监督域适应的基本假设是,学习到域不可知特征空间的映射,以及在源域上表现足够好的分类器,可以泛化到目标域。 然而,最近的研究 [20,42,46,7] 表明,这些条件不足以实现成功的域适应,
甚至可能由于两个域的边缘标签分布之间的差异而损害泛化

半监督学习 (SSL) [1,36,3,45] 已被证明在每个注释的性能方面非常高效,因此提供了一种更经济的方式来训练深度学习模型。 然而,一般来说,UDA 方法在半监督环境中表现不佳,在半监督环境中我们可以访问目标域中的一些标记样本 [31]。 半监督域适应 (SSDA) [35,21,19],利用目标域中的少量标记样本来帮助学习目标域上具有低错误率的模型。 然而,如 [19] 所示,简单地将标记的目标样本与标记的源样本对齐会导致目标域中的域内差异。 在训练期间,标记的目标样本被拉向相应的源样本簇。然而,未标记的样本与标记目标样本的较小相关性被抛在后面。这是因为标记源样本的数量支配标记目标样本的数量,导致标签分布偏斜。 这导致在目标域的同一类中进行子分布。 为了减轻来自源域和目标域的标记样本之间的这种偏差比率,最近的方法 [17,40] 将伪标签分配给未标记的数据。 但是,这些伪标签可能存在噪声,可能导致对目标域的泛化效果不佳。

在本文中,我们提出了一种简单而有效的方法来缓解 SSDA 面临的上述挑战。 为了对齐来自两个域的监督样本,我们利用对比损失来学习语义有意义和域不变的特征空间。 为了解决域内差异问题,我们通过将未标记目标样本的特征表示与标记样本的特征表示进行比较来计算未标记目标样本的软伪标签。 然而,与标记样本相关性较低的样本可能会有噪声和不正确的伪标签。 因此,我们根据模型对各个伪标签的置信度,在整个训练过程中逐渐将伪标记样本注入(或从)标记目标数据集中(或从中移除)。

3 方法

整体框架:

Support set:
基于小批量,源域、目标域标记样本每个类包含  $\eta_{\text {sup }}$ 个样本,所以支持集包含来自两个域的 $\eta_{\text {sup }} C$ 个样本,总共 2 个 $\eta_{\text {sup }} C$ 个样本。

3.1 域间特征对齐

直接地利用对比损失,通过明确地将相同类别的样本视为正样本,而不管领域如何。 然后训练特征提取器通过最大化同一类特征之间的相似性来最小化 $\mathcal{L}_{\text {con }}$。

$\mathcal{L}_{\text {con }}=\sum_{i \in A} \frac{-1}{\left|P_{i}\right|} \sum_{p \in P_{i}} \log \frac{\exp \left(z_{i} \cdot z_{p} / \tau\right)}{\sum_{a \in A \backslash i} \exp \left(z_{a} \cdot z_{p} / \tau\right)} \quad\quad\quad(1)$

Note
:支撑集之间;

3.2 伪标签注入

然而,正如 [19] 中指出的那样,对齐来自源域和目标域的标记样本可能会导致目标域中的子分布。 即,与目标域中标记样本相关性较低的未标记样本不会受到对比损失的影响。 这会导致域内差异,从而导致性能不佳。 为缓解这个问题,本文考虑将未标记的样本注入标记的目标数据集 ^ T,从而有效地增加目标域中标记样本的支持。 我们将更详细地讨论这种方法。 3.2.

为了减少域内差异,我们建议将未标记目标数据集 $T$ 中的样本注入标记目标数据集 $\hat{T}$。 使用支持集,首先计算未标记样本的软伪标签。在整个训练过程中,我们为未标记的目标数据集 $T$ 中的每个样本保留锐化软伪标签的指数移动平均值。 这个移动平均值估计了我们的模型对每个未标记样本的预测的置信度。 使用这个估计,我们将高度置信的样本注入到标记的目标数据集 $\hat{T}$ 中,并且在每个时期之后将它们各自的标签设置为主导类。

为了计算来自目标域的未标记样本的软伪标签,我们从 PAWS [1] 中获得灵感,这是一项半监督学习的最新工作,并将其扩展到 SSDA 设置。 我们将支持集 $\mathcal{x}_{sup} $ 及其各自的标签表示为 $y_{sup}$。 设 $z_{sup}$ 是支持集 $\mathcal{x}_{sup} $ 中样本的归一化特征表示,$\hat{z}_{i}\left(=z_{i} /\left\|z_{i}\right\|\right)$ 表示未标记样本 $x_i$ 的归一化特征表示。 然后,可以使用以下方法计算第 $i$ 个未标记样本的软伪标签:

$\tilde{y}_{i}=\sigma_{\tau}\left(\hat{z}_{i} \cdot \hat{z}_{\text {sup }}^{\top}\right) y_{\text {sup }}$

其中,$\sigma_{\tau}(\cdot)$ 表示带温度参数 $\tau$ 的 $\text{softmax}$, 然后使用温度 $\tau>0$ 的锐化函数 $\pi$ 对这些软伪标签进行锐化,描述如下:

$\pi(\tilde{y})=\frac{\tilde{y}^{1 / \tau}}{\sum_{j=1}^{C} \tilde{y}_{j}^{1 / \tau}}$

锐化有助于从未标记和标记样本之间的相似性度量中产生自信的预测。

在整个训练过程中,我们保持未标记目标数据集 $T$ 中每个图像的锐化软伪标签的指数移动平均值 (EMA)。 更具体地说,我们维护一个映射 $\mathcal{P}: \mathbb{I} \rightarrow \mathbb{R}^{C}$ 从未标记样本的图像 ID 到它们各自锐化的软伪标签(类概率分布)的运行 EMA。 令 $ID(\cdot)$ 表示一个运算符,它返回与未标记目标数据集 $T$ 中的输入样本对应的图像 $ID$,$\mathcal{P}\left(\operatorname{ID}\left(x_{i}\right)\right)$ 是 $x_i$ 的锐化伪标签的 EMA。 然后,未标记数据集 $T$ 中样本 $x_i$ 的指数移动平均值更新如下:

$\mathcal{P}\left(\mathrm{ID}\left(x_{i}\right)\right) \leftarrow \rho \pi\left(\tilde{y}_{i}\right)+(1-\rho) \mathcal{P}\left(\operatorname{ID}\left(x_{i}\right)\right)   \quad\quad(5)$

其中 $\rho$ 表示动量参数。 当在训练过程中第一次遇到一个样本时,$\mathcal{P}\left(\operatorname{ID}\left(x_{i}\right)\right)$ 被设置为 $\pi\left(\tilde{y}_{i}\right)$ 和  $\text{Eq.5}$ 之后使用。

在每个 epoch 之后,我们检查 $\mathcal{P}$ 中每个样本的 EMA(类概率分布)。如果某个特定样本对某个类的置信度超过某个阈值 $\gamma$,我们将该样本及其对应的预测类注入到标记的目标数据集 $\hat{T}$ 中。 我们将考虑用于注射 $I$ 的样本集定义为:

$I_{t} \triangleq\left\{\left(x_{i}, \arg \max \mathcal{P}\left(\operatorname{ID}\left(x_{i}\right)\right) \mid x_{i} \in T \wedge \max \mathcal{P}\left(\operatorname{ID}\left(x_{i}\right)\right) \geq \gamma\right\}\right.\quad\quad\quad(6)$

其中 $t$ 表示当前 $\text{epoch}$。

但是,这些样本可能存在噪音并可能阻碍训练过程; 因此,如果样本的置信度低于阈值 $\gamma$,我们也会从标记的数据集中删除样本。 要从标记目标数据集 $R$ 中删除的样本集定义为:

$R_{t} \triangleq\left\{\left(x_{i}, y_{i}\right) \mid x_{i} \in\left(\hat{T}_{t} \backslash \hat{T}_{0}\right) \wedge \max \mathcal{P}\left(\operatorname{ID}\left(x_{i}\right)\right)<\gamma\right\}$

其中 $y_{i}$ 表示先前分配给方程式中的样本 $x_i$ 的相应伪标签。 请注意,来自标记目标数据集 $\hat{T}_{0}$ 的原始样本永远不会从数据集中删除,因为 $I$ 和 $R$ 都仅包含来自未标记目标数据集 $T$ 的样本。

因此,在每个纪元 $t$ 之后,标记的目标数据集 $T$ 将更新为:

$\hat{T}_{t+1}=\left\{\begin{array}{ll}\left(\hat{T}_{t} \backslash R_{t}\right) \cup I_{t} & \text { if } t \geq W \\\hat{T}_{t} & \text { otherwise }\end{array}\right.$

其中 $W$ 表示标记的目标数据集 $\hat{T}$ 保持不变的预热阶段数。 这些预热时期允许源域和目标域的特征表示在样本被注入标签目标数据集之前在某种程度上对齐。 这可以防止假阳性样本进入 $\hat{T}$,否则会阻碍学习过程。

3.3 实例级相似度

我们现在介绍实例级相似性损失。 受 [1,5] 的启发,我们遵循多视图增强来生成未标记图像的 $ηg = 2$ 全局裁剪和 $ηl$ 局部裁剪。 这种增强方案背后的关键见解是通过明确地使这些不同视图的特征表示更接近来强制模型关注感兴趣的对象。 全局裁剪包含更多关于感兴趣对象的语义信息,而局部裁剪仅包含图像(或对象)的有限视图。 通过计算全局作物和支持集样本之间的特征级相似度,我们使用 $\text{Eq.3}$ 计算未标记样本的伪标签。

然后训练特征提取器以最小化使用一个全局视图生成的伪标签与使用另一个全局视图生成的锐化伪标签之间的交叉熵。 此外,使用局部视图生成的伪标签与来自全局视图的锐化伪标签的平均值之间的交叉熵被添加到损失中。

稍微滥用符号,给定样本 $x_{i}$,我们将 $\tilde{y}_{i}^{g_{1}}$ 和 $\tilde{y}_{i}^{g_{2}}$ 定义为两种全局作物的伪标签,并且 $\tilde{y}_{i}^{l_{j}}$ 表示第 $j$ 个局部作物的伪标签。 类似地,我们遵循相同的符号来为这些由 $\pi$ 表示的作物定义锐化的伪标签。 因此训练特征提取器以最小化以下损失:

$\mathcal{L}_{i l s}=-\sum\limits_{i=1}^{\left|B_{u}\right|}\left(\mathrm{H}\left(\tilde{y}_{i}^{g_{1}}, \pi_{i}^{g_{2}}\right)+\mathrm{H}\left(\tilde{y}_{i}^{g_{2}}, \pi_{i}^{g_{1}}\right)+\sum\limits _{j=1}^{\eta_{l}} \mathrm{H}\left(\tilde{y}_{i}^{l_{j}}, \pi_{i}^{g}\right)\right),$

其中,$\mathrm{H}(\cdot, \cdot)$ 表示交叉熵,$\pi_{i}^{g}=\left(\pi_{i}^{g_{1}}+\pi_{i}^{g_{1}}\right) / 2$,$\left|B_{u}\right|$ 表示未标记样本的数量。

3.4 域内对齐

为了确保来自目标域中同一类的未标记样本在潜在空间中靠得更近,我们使用未标记样本之间的一致性损失。 由于这些样本没有标签,我们计算未标记样本之间的成对特征相似性,以估计它们是否可能属于同一类。 正如[13]所提出的,如果两个样本 $x_i$ 和 $x_j$ 的前 $k$ 个高度激活的特征维度的索引相同,则可以认为它们相似。 令 $top-k (z)$ 表示 $z$ 的前 $k$ 个高度激活的特征维度的索引集,然后,我们认为两个未标记的样本 $i$ 和 $j$ 相似,如果:

$\text { top-k }\left(z_{i}\right) \ominus \text { top- } \mathrm{k}\left(z_{j}\right)=\Phi$

其中,$z_{i}$ 和 $z_{j}$ 是各自的特征表示,$\ominus$ 是对称集差算子。

我们构造一个二元矩阵 $M \in\{0,1\}^{\left|B_{u}\right| \times\left|B_{u}\right|}$ ,$M_{i j}$ 表示未标记 Batch $B_{u}$ 中第 $i$ 个样本是否与第 $j$ 个样本相似。使用相似性矩阵 $M$ ,我们计算目标未标记样本的域内一致性损失 $\mathcal{L}_{i d a}$ 如下:

$\mathcal{L}_{i d a}=\frac{1}{\left|B_{u}\right|^{2}} \sum_{i=1}^{\left|B_{u}\right|} \sum_{j=1}^{\left|B_{u}\right|} M_{i j}\left\|z_{i}-z_{j}\right\|_{2}$

3.5 分类损失和整体框架

我们使用标签平滑交叉熵 [24] 损失来训练分类器层。 对于分类器训练,我们只使用来自标记的源数据集 $S$ 和标记的目标数据集 $\hat{T}$ 的样本,这些样本不断用新样本更新。

$\mathcal{L}_{c l s}=-\sum_{i=1}^{2 \eta_{\text {sup }} C} \mathrm{H}\left(h_{i}, \hat{y}_{i}\right)$

其中,$h_{i}$ 是预测的类别概率,$H$ 表示交叉熵损失,$\hat{y}_{i}=(1-\alpha) y_{i}+\alpha / C$ 是对应于 $xi$ 的平滑标签。 这里,$\alpha$ 是平滑参数,$y_{i}$ 是单热编码标签向量。

结合我们提出的方法 SPI、$\mathcal{L}_{\text {con }}$、$\mathcal{L}_{i l s}$ 和 $\mathcal{L}_{i d a$ 中使用的不同损失,产生一个单一的训练目标:

$\mathcal{L}_{S P I}=\lambda \mathcal{L}_{c o n}+\mathcal{L}_{i l s}+\mathcal{L}_{i d a}+\mathcal{L}_{c l s}$

4 实验

消融研究

5 总结

为了对齐两个域,使用两个域的监督样本利用对比损失来来学习语义上有意义和域不可知的特征空间;

为减轻标签比例偏斜带来的挑战,通过将未标记的目标样本的特征表示 与 来自源域和目标域的标记样本的特征表示进行比较来为未标记的目标样本打伪标记;

为增加对目标域的支持,潜在的噪声伪标签在训练过程中逐渐注入到标记的目标数据集中。 具体来说,使用温度标度余弦相似性度量来为未标记的目标样本分配软伪标签。 此外,为每个未标记的样本计算软伪标签的指数移动平均值。 这些伪标签基于置信度阈值逐渐注入(或移除)到(从)标记的目标数据集中,以补充源和目标分布的对齐。 最后,在标记和伪标记数据集上使用监督对比损失来对齐源和目标分布。