2024年11月

1.概述

MoE代表“混合专家模型”(Mixture of Experts),这是一种架构设计,通过将不同的子模型(即专家)结合起来进行任务处理。与传统的模型相比,MoE结构能够动态地选择并激活其中一部分专家,从而显著提升模型的效率和性能。尤其在计算和参数规模上,MoE架构能够在保持较低计算开销的同时,扩展模型的能力,成为许多LLM的热门选择。

2.内容

本篇内容主要介绍MoE架构的两个核心组件:专家(Experts)和路由器(Router)。这两个组件在典型的基于LLM的架构中发挥着关键作用。专家是负责执行特定任务的子模型,而路由器则负责决定哪些专家需要在给定任务中被激活,从而优化计算效率和模型表现。

2.1 什么是专家混合

混合专家模型(MoE)是一种创新技术,通过将多个专门的子模型(即“专家”)集成进大型语言模型(LLM)中,显著提升了模型的性能和效率。

MoE架构的核心由以下两个部分构成:

  • 专家(Experts)
    :每个前馈神经网络层都配备了一组专家,这些专家是模型中的独立子网络,每次计算时,只有一部分专家会被激活参与处理。专家通常是具备特定任务能力的神经网络。
  • 路由器(Router)或门控网络(Gate Network)
    :该组件负责根据输入数据(如令牌)决定哪些专家会处理特定任务。路由器根据模型的输入动态地选择最适合的专家,从而提高计算效率。

通过这种结构,MoE架构不仅在保持较低计算成本的同时,能够处理更多样化、复杂的任务,也能够根据需求扩展模型规模,从而提升LLM的整体表现。

值得注意的是,MoE中的“专家”并非专注于某一特定领域,如“心理学”或“生物学”。相反,这些专家主要专注于学习词汇层面的句法结构,而不是领域知识。因此,每个专家的作用更偏向于捕捉语言的基本模式和结构,而非专业领域的深度理解。

更具体地说,MoE中的“专家”专注于在特定上下文中处理特定的令牌。路由器(门控网络)根据输入数据的特征,动态选择最适合处理该数据的专家。这种机制使得模型能够根据任务需求灵活地调用不同的专家,从而提高处理效率和准确性。

每个“专家”并不是独立的完整大型语言模型,而是嵌入在LLM架构中的一个子模型组件。它们与其他专家协同工作,共同提升整个模型的处理能力和效率。

2.2 密集层

为了更好地理解专家的作用以及它们的工作原理,我们首先需要了解MoE所替代的传统架构——密集层(Dense Layers)。

混合专家模型(MoE)构建于大型语言模型(LLM)的基本组成部分之一——前馈神经网络(FFNN)之上。需要注意的是,在标准的解码器-only Transformer架构中,FFNN通常在层归一化后被应用到每一层中。这是LLM处理信息的核心方式之一,而MoE的引入旨在通过引入多个专家模型替代传统的密集层,提升模型的表达能力和计算效率。

前馈神经网络(FFNN)使模型能够基于注意力机制提取的上下文信息,进一步处理并捕捉数据中的复杂关系。然而,FFNN的规模随着层数增加而迅速膨胀。为了能够学习这些复杂的关系,FFNN通常会在输入信息的基础上进行扩展,这可能导致计算开销的大幅增加。

2.3 稀疏层

在传统的Transformer架构中,前馈神经网络(FFNN)被称为密集模型,因为它的所有参数(包括权重和偏置)都会被激活并用于计算输出,没有任何信息被忽略。仔细分析密集模型时,我们可以看到,输入信息会激活所有的参数,每个参数在计算中都会发挥作用,从而导致计算复杂度和资源消耗较大。

与密集模型不同,稀疏模型只激活一部分参数,这种方式与混合专家模型(MoE)紧密相连。具体来说,我们可以将密集模型拆分为多个“专家”子模型,重新训练它们,并在每次任务中仅激活一部分专家进行计算。这种方法不仅能减少计算开销,还能提高模型处理特定任务时的效率。

这种方法的核心理念是,每个专家在训练过程中专注于学习不同的信息。到了推理阶段,根据任务的具体需求,系统只会激活最相关的专家。这样,面对不同的问题时,我们能够选择最适合的专家来进行高效处理。

2.4 专家学习的内容

如前所述,专家并非专注于学习某一完整领域的知识,而是专注于捕捉更细粒度的信息。因此,将它们称为“专家”有时会让人误解,因为这些专家并不具备传统意义上在某一领域的深度专业知识。

在解码器模型中,专家似乎并未表现出相同类型的专业化。尽管如此,这并不意味着所有专家在作用上是相同的。一个很好的例子可以参考《
Mixtral 8x7B
》论文,其中每个令牌都被标注为其首选专家,从而展示了专家选择的多样性和灵活性。

上图还展示了专家们更倾向于关注句法结构,而非特定领域的知识。因此,尽管解码器中的专家没有明确的专业化,它们在处理某些类型的令牌时却表现出一致性和特定的应用模式。

2.5 专家架构

虽然将专家看作是密集模型中的隐藏层并将其拆分成若干部分进行可视化是一个有趣的方式,但实际上,专家通常是独立的、完整的前馈神经网络(FFNN)。每个专家在模型中扮演着独立的角色,执行特定的计算任务。

由于大多数大型语言模型(LLM)包含多个解码器层,一个输入文本通常会在生成过程中经过多个专家的处理,每个专家负责不同的任务或处理不同的特征。这样,模型可以更有效地捕捉复杂的语言模式和语境信息。

由于每个令牌可能会激活不同的专家,这导致模型在处理每个令牌时可能会选择不同的“路径”。这种动态选择使得模型能够灵活地根据不同的上下文需求进行优化。

更新后的解码器模块可视化将显示更多的前馈神经网络(FFNN),每个FFNN代表一个专家。这样,每个专家都拥有独立的计算路径,以便在处理不同任务时提供更具针对性的计算能力。

3.路由

有了专家模型后,模型如何确定使用哪些专家呢?在专家之前,加入了一个路由器(或称门控网络),其作用是根据输入令牌的特征来决定激活哪个专家。路由器是一个前馈神经网络(FFNN),它会输出一组概率值,根据这些概率值来选择最适合当前任务的专家。

有了专家模型后,模型如何确定使用哪些专家呢?在专家之前,加入了一个路由器(或称门控网络),其作用是根据输入令牌的特征来决定激活哪个专家。路由器是一个前馈神经网络(FFNN),它会输出一组概率值,根据这些概率值来选择最适合当前任务的专家。

MoE层有两种实现方式:稀疏型和密集型混合专家模型。两者都依赖路由器来选择专家,但稀疏型MoE只激活少数几个专家,而密集型MoE则激活所有专家,只不过激活的比例和分布可能不同。这种设计使得模型在处理不同任务时可以灵活调节计算资源的分配。

例如,给定一组令牌,普通的MoE会将令牌分配给所有专家,而稀疏型MoE则只激活少数几个专家。在现有的LLM中,提到“MoE”时,通常指的是稀疏型MoE,因为它通过仅激活一部分专家来减少计算开销,这对于大型语言模型的高效运行至关重要。

门控网络可以说是MoE中最为重要的部分,因为它不仅在推理时决定选择哪些专家,而且在训练过程中也起着关键作用。在最简单的形式下,输入(x)与路由器的权重矩阵(W)相乘,生成一个加权的输出,用于决定激活哪些专家。

然后,我们对输出应用SoftMax函数,将其转化为每个专家的概率分布G(x)。这个概率分布决定了每个专家被选中的可能性,从而指导路由器选择最合适的专家进行处理。

路由器根据概率分布选择最匹配的专家来处理给定的输入。最终,模型将每个选定专家的输出与相应的路由器概率相乘,并将所有结果相加,得到最终的输出。

然而,这种简单的机制可能会导致路由器频繁选择相同的专家,因为某些专家的学习速度可能快于其他专家,从而使得它们在选择过程中占据主导地位。

这种不均衡的选择不仅会导致某些专家被频繁激活,而其他专家几乎没有机会参与训练,还会引发训练和推理阶段的问题。因此,我们希望在训练和推理过程中保持专家间的平衡,这就是所谓的负载均衡。负载均衡有助于避免某些专家过度拟合,提高模型的多样性和泛化能力。

4.小节

至此,我们的混合专家模型(MoE)之旅圆满结束!希望这篇文章能帮助你更好地理解这种创新技术的潜力。如今,几乎所有的模型架构都包含了至少一种MoE变体,MoE看起来将成为未来技术中的重要组成部分。

5.结束语

这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

另外,博主出新书了《
深入理解Hive
》、同时已出版的《
Kafka并不难学
》和《
Hadoop大数据挖掘从入门到进阶实战
》也可以和新书配套使用,喜欢的朋友或同学, 可以
在公告栏那里点击购买链接购买博主的书
进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。

书接上回,我们今天开始实现对象集合与DataTable的相互转换。

01
、接口设计

上文中已经详细讲解了整体设计思路以及大致设计了需要哪些方法。下面我们先针对上文设计思想确定对外提供的接口。具体接口如下:

//根据列名数组创建表格
public static DataTable Create(string[] columnNames, string? tableName = null);

//根据列名-类型键值对创建表格
public static DataTable Create(Dictionary<string, Type> columns, string? tableName = null);

//根据类创建表格
//如果设置DescriptionAttribute,则将特性值作为表格的列名称
//否则将属性名作为表格的列名称
public static DataTable Create<T>(string? tableName = null);

//把表格转换为实体对象集合
//如果设置DescriptionAttribute,则将特性值作为表格的列名称
//否则将属性名作为表格的列名称
public static IEnumerable<T> ToModels<T>(DataTable dataTable);

//把实体对象集合转为表格
//如果设置DescriptionAttribute,则将特性值作为表格的列名称
//否则将属性名作为表格的列名称
public static DataTable ToDataTable<T>(IEnumerable<T> models, string? tableName = null);

//把一维数组作为一列转换为表格
public static DataTable ToDataTableWithColumnArray<TColumn>(TColumn[] array, string? tableName = null, string? columnName = null);

//把一维数组作为一行转换为表格
public static DataTable ToDataTableWithRowArray<TRow>(TRow[] array, string? tableName = null);

//行列转置
public static DataTable Transpose(DataTable dataTable, bool isColumnNameAsData = true);

02
、根据列名数组创建表格

该方法实现比较简单,我们直接看代码:

//根据列名数组创建表格
public static DataTable Create(string[] columnNames, string? tableName = null)
{
    var table = new DataTable(tableName);
    foreach (var columnName in columnNames)
    {
        table.Columns.Add(columnName);
    }
    return table;
}

我们进行一个简单的单元测试:

[Fact]
public void Create()
{
    //正常创建成功
    var columnNames = new string[] { "A", "B" };
    var table = TableHelper.Create(columnNames);
    Assert.Equal("", table.TableName);
    Assert.Equal(2, table.Columns.Count);
    Assert.Equal(columnNames[0], table.Columns[0].ColumnName);
    Assert.Equal(columnNames[1], table.Columns[1].ColumnName);
    Assert.Equal(typeof(string), table.Columns[0].DataType);
    Assert.Equal(typeof(string), table.Columns[1].DataType);

    //验证表名
    table = TableHelper.Create(columnNames, "test");
    Assert.Equal("test", table.TableName);

    //验证列名不能重复
    columnNames = new string[] { "A", "A" };
    Assert.Throws<DuplicateNameException>(() => TableHelper.Create(columnNames));
}

03
、根据列名-类型键值对创建表格

此方法是上一个方法的补充,默认直接根据列名创建表格,则所有列的数据类型都是string类型,而此方法可以指定每列的数据类型,实现也很简单,代码如下:

//根据列名-类型键值对创建表格
public static DataTable Create(Dictionary<string, Type> columns, string? tableName = null)
{
    var table = new DataTable(tableName);
    foreach (var column in columns)
    {
        table.Columns.Add(column.Key, column.Value);
    }
    return table;
}

04
、根据类创建表格

该方法是将类的属性名作为表格的列名称,属性对应的类型作为表格列的数据类型,把类转为表格。

同时我们需要约束类只能为结构体或类,而不能是枚举、基础类型、以及集合类型、委托、接口等。显然我们很难通过泛型约束达到我们的需求,因此我们首先需要对该方法的泛型进行类型校验。

校验成功后,我们只需要通过反射即可拿到类的所有属性信息,即可创建表格。同时我们约定如果属性设置了DescriptionAttribute特性,则特性值作为列名,如果没有设置特性则取属性名称作为列名。

//根据类创建表格
//如果设置DescriptionAttribute,则将特性值作为表格的列名称
//否则将属性名作为表格的列名称
public static DataTable Create<T>(string? tableName = null)
{
    //T必须是结构体或类,并且不能是集合类型
    AssertTypeValid<T>();
    //获取类的所有公共属性
    var properties = typeof(T).GetProperties();
    var columns = new Dictionary<string, Type>();
    foreach (var property in properties)
    {
        //根据属性获取列名
        var columnName = GetColumnName(property);
        //组织列名-类型键值对
        columns.Add(columnName, property.PropertyType);
    }
    return Create(columns, tableName);
}

//断言类型有效性
private static void AssertTypeValid<T>()
{
    var type = typeof(T);
    if (type.IsValueType && !type.IsEnum && !type.IsPrimitive)
    {
        //是值类型,但是不是枚举或基础类型
        return;
    }
    else if (typeof(T).IsClass && !typeof(IEnumerable).IsAssignableFrom(typeof(T)))
    {
        //是类类型,但是不是集合类型,同时也不是委托、接口类型
        return;
    }
    throw new InvalidOperationException("T must be a struct or class and cannot be a collection type.");
}

//根据属性获取列名称
private static string GetColumnName(PropertyInfo property)
{
    //获取描述特性
    var attribute = property.GetCustomAttribute<DescriptionAttribute>();
    //如果存在描述特性则返回描述,否则返回属性名称
    return attribute?.Description ?? property.Name;
}

下面我们针对枚举、字符串,基础类型、集合等情况进行详细的单元测试,代码如下:

[Fact]
public void Create_T()
{
    //验证枚举
    var expectedMessage = "T must be a struct or class and cannot be a collection type.";
    var exception1 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<StatusEnum>());
    Assert.Equal(expectedMessage, exception1.Message);

    //验证基础类型
    var exception2 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<int>());
    Assert.Equal(expectedMessage, exception2.Message);

    //验证字符串类型
    var exception3 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<string>());
    Assert.Equal(expectedMessage, exception3.Message);

    //验证集合
    var exception4 = Assert.Throws<InvalidOperationException>(() => TableHelper.Create<Dictionary<string, Type>>());
    Assert.Equal(expectedMessage, exception4.Message);

    //验证正常情况
    var table = TableHelper.Create<Student<double>>();
    Assert.Equal("", table.TableName);
    Assert.Equal(3, table.Columns.Count);
    Assert.Equal("标识", table.Columns[0].ColumnName);
    Assert.Equal("姓名", table.Columns[1].ColumnName);
    Assert.Equal("Age", table.Columns[2].ColumnName);
    Assert.Equal(typeof(string), table.Columns[0].DataType);
    Assert.Equal(typeof(string), table.Columns[1].DataType);
    Assert.Equal(typeof(double), table.Columns[2].DataType);
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Ideal

本文使用python语言实现了一个端口转发的程序,该程序可以实现多网络之间的信息通信,当然这里有个前提,那就是多个网络都在一台主机上有可以连通的端口。


之所以有这个编写代码的需求,是因为最近使用的science network工具不大好用了,于是就要博士同学发给我一个好用些的来,固然发现同学用的那个工具更好用,效果如下:

image-20241130090741916


虽然这个工具好用,但是用着用着就发现了问题,那就是这个工具只能支持本机上网,而和其他的同类工具不同,其他的同类工具都是可以支持局域网中其他主机的请求的,而这个就导致了一定的问题,比如我需要使用GitHub,使用huggingface,等等,而我一般都是在workstation上用这些应用的,而workstation上的系统又是Linux系统,而这个朋友发给我的这个工具又是只能运行在Windows系统上的,并且最为可气的是这个工具只接收localhost的端口转发,而不能只是局域网中其他主机的请求的,这样就导致我的工作电脑(Linux系统)是无法通过这个工具来连接huggingface这样的应用的,为此就想到了自己编写一个代码来实现这中间的gap。


一开始想的是自己手动编写这样的代码,但是考虑到比较耗时,并且个人使用,也不需要什么代码优化,也不追求什么性能,于是就想到了使用ChatGPT来自动生成一个,于是得到了下面的代码:

import socket
import threading

# 转发函数
def forward(source, destination):
    while True:
        try:
            data = source.recv(4096)
            if not data:
                break
            destination.sendall(data)
        except Exception as e:
            print(f"Connection error: {e}")
            break

# 处理单个客户端连接
def handle_client(client_socket, target_host, target_port):
    try:
        # 连接到目标地址
        target_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        target_socket.connect((target_host, target_port))
        
        # 创建两个线程:分别转发客户端到目标和目标到客户端的数据
        threading.Thread(target=forward, args=(client_socket, target_socket)).start()
        threading.Thread(target=forward, args=(target_socket, client_socket)).start()
    except Exception as e:
        print(f"Error handling client: {e}")
        client_socket.close()

# 主端口转发逻辑
def start_port_forwarding(local_host, local_port, target_host, target_port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((local_host, local_port))
    server.listen(15)
    print(f"[*] Listening on {local_host}:{local_port} and forwarding to {target_host}:{target_port}")

    while True:
        client_socket, addr = server.accept()
        print(f"[*] Accepted connection from {addr}")
        threading.Thread(target=handle_client, args=(client_socket, target_host, target_port)).start()

if __name__ == "__main__":
    # 配置端口转发信息
    LOCAL_HOST = "0.0.0.0"  # 本地监听地址
    LOCAL_PORT = 8888       # 本地监听端口
    TARGET_HOST = "127.0.0.1"  # 目标地址(替换为实际地址)
    TARGET_PORT = 33210           # 目标端口

    # 启动端口转发
    start_port_forwarding(LOCAL_HOST, LOCAL_PORT, TARGET_HOST, TARGET_PORT)

事实证明ChatGPT自动生成的这个端口转发代码还是比较好用的,这样就可以在个人手机上也可以看YouTube了。


这里需要注意的是端口的设置,我们可以看到下图中这个工具的本地接受的端口号为HTTP下的33210,于是在我们的这个代码中就需要将TARGET_PORT设置为33210,由于是本地的端口转发,因此本地的IP设置为127.0.0.1,由于我们的这个代码实现的是对局域网中的请求的接收并转发给TARGET_PORT,因此我们的LOCAL_HOST需要设置为0.0.0.0,这样就可以接收局域网中的请求,而如果设置为“127.0.0.1”,那么依然无法实现对局域网中请求的接收。我们的LOCAL_PORT的设置可以比较随意,这个端口号是暴露给局域中的其他主机进行网络设置时使用的。

image-20241130091546968


其他主机上(局域网中其他主机)的网络设置:

image-20241130092119258

这里的IP:192.168.1.110,就是运行这个代码和这个science network的工具的Windows主机的IP地址。



个人github博客地址:
https://devilmaycry812839668.github.io/

前言:
大型语言模型(LLMs)的发展历程可以说是非常长,从早期的GPT模型一路走到了今天这些复杂的、公开权重的大型语言模型。最初,LLM的训练过程只关注预训练,但后来逐步扩展到了包括预训练和后训练在内的完整流程。后训练通常涵盖监督指导微调和对齐过程,而这些在ChatGPT的推广下变得广为人知。

自ChatGPT首次发布以来,训练方法学也在不断进化。在这几期的文章中,我将回顾近1年中在预训练和后训练方法学上的最新进展。

关于LLM开发与训练流程的概览,特别关注本文中讨论的新型预训练与后训练方法

每个月都有数百篇关于LLM的新论文提出各种新技术和新方法。然而,要真正了解哪些方法在实践中效果更好,一个非常有效的方式就是看看最近最先进模型的预训练和后训练流程。幸运的是,在近1年中,已经有四个重要的新型LLM发布,并且都附带了相对详细的技术报告。

在本文中,我将重点介绍以下模型中的Meta的 Llama 3.1语言模型 预训练和后训练流程:

• 阿里巴巴的 Qwen 2

• 苹果的 智能基础语言模型

• 谷歌的 Gemma 2

• Meta AI 的 Llama 3.1

我会完整的介绍列表中的全部模型,但介绍顺序是基于它们各自的技术论文在arXiv.org上的发表日期,这也巧合地与它们的字母顺序一致。

4. Meta AI的Llama 3.1

Meta发布的Llama LLM新版本总是大事。这次发布伴随着一份92页的技术报告:《Llama 3模型群》。最后,在本节中,我们将查看上个月发布的第四份重要模型论文。

4.1 Llama 3.1概述

除了发布一个巨大的4050亿参数模型,Meta还更新了他们之前的80亿和700亿参数模型,使它们在MMLU性能上略有提升。

                                      MMLU基准测试不同模型的表现。

虽然Llama 3像其他最近的LLM一样使用群组查询注意力,但令人惊讶的是,Meta AI拒绝使用滑动窗口注意力和混合专家方法。换句话说,Llama 3.1看起来非常传统,重点显然放在预训练和后训练上,而非架构创新。

与之前的Llama版本相似,它的权重是公开可用的。此外,Meta表示他们更新了Llama 3的许可证,现在终于可以(被允许)使用Llama 3进行合成数据生成或知识蒸馏以改善其他模型。

4.2 Llama 3.1预训练

Llama 3在庞大的15.6万亿标记数据集上进行训练,这比Llama 2的1.8万亿标记有显著增加。研究人员表示,它支持至少八种语言(而Qwen 2能处理20种语言)。

Llama 3的一个有趣方面是它的词汇量为128,000,这是使用OpenAI的tiktoken分词器开发的。(对于那些对分词器性能感兴趣的人,我在这里做了一个简单的基准比较。)

在预训练数据质量控制方面,Llama 3采用了基于启发式的过滤以及基于模型的质量过滤,利用像Meta AI的fastText和基于RoBERTa的分类器这样的快速分类器。这些分类器还有助于确定训练期间使用的数据混合的上下文类别。

Llama 3的预训练分为三个阶段。第一阶段涉及使用15.6万亿标记进行标准的初始预训练,上下文窗口为8k。第二阶段继续预训练,但将上下文长度扩展到128k。最后一个阶段涉及退火,进一步提高模型的性能。让我们更详细地看看这些阶段。

4.2.1 预训练I:标准(初始)预训练

在他们的训练设置中,他们开始使用由4096个序列长度的400万标记组成的批次。这意味着批量大小大约为1024标记,假设400万这个数字是四舍五入得到的。在处理了首批252百万标记后,他们将序列长度增加到8192。在训练过程中,处理了2.87万亿标记后,他们再次将批量大小翻倍。

此外,研究人员并没有在整个训练过程中保持数据混合不变。相反,他们调整了训练过程中使用的数据混合,以优化模型学习和性能。这种动态处理数据的方法可能有助于改善模型跨不同类型数据的泛化能力。

**4.2.2 预训练II:继续预训练以延长上下文 **

与其他一步增加其上下文窗口的模型相比,Llama 3.1的上下文延长是一个更渐进的方法:在这里,研究人员通过六个不同阶段将上下文长度从8000增加到128000标记。这种分步增加可能让模型更平滑地适应更大的上下文。

此过程使用的训练集涉及8000亿标记,约占总数据集大小的5%。

4.2.3 预训练III:在高质量数据上退火

对于第三阶段预训练,研究人员在一个小但高质量的混合上训练模型,他们发现这有助于改善基准数据集上的性能。例如,在GSM8K和MATH训练集上退火提供了在相应的GSM8K和MATH验证集上的显著提升。

在论文的3.1.3节中,研究人员表示退火数据集大小为400亿标记(总数据集大小的0.02%);这400亿退火数据集用于评估数据质量。在3.4.3节中,他们表示实际的退火只在4000万标记上进行(退火数据的0.1%)。

                                    Llama 3.1预训练技术总结。

4.3 Llama 3.1后训练

在后训练过程中,Meta AI团队采用了一种相对简单的方法,包括监督微调(SFT)、拒绝采样和直接偏好优化(DPO)。

他们观察到,像带PPO的RLHF这样的强化学习算法与这些技术相比,稳定性较低且更难扩展。值得注意的是,SFT和DPO步骤在多轮中反复迭代,融合了人工生成和合成数据。

在描述更多细节之前,他们的工作流程如下图所示。

                                      来自Llama 3.1论文的带注释的图表,描述了后训练过程。

请注意,尽管他们使用了DPO,但他们也开发了一个奖励模型,就像在RLHF中一样。起初,他们使用预训练阶段的一个检查点训练奖励模型,利用人工标注数据。这个奖励模型随后被用于拒绝采样过程,帮助选择适当的提示进行进一步训练。

在每一轮训练中,他们不仅对奖励模型,还对SFT和DPO模型应用了模型平均技术。这种平均涉及合并最近和以前模型的参数,以稳定(并改善)随时间的性能。

对于那些对模型平均的技术细节感兴趣的人,我在我早期文章《理解模型融合和权重平均》的部分中讨论了这个话题,文章标题为《模型融合、专家混合,以及向更小LLM的迈进》。

总之,核心是一个相对标准的SFT + DPO阶段。然而,这个阶段在多轮中重复。然后,他们在拒绝采样中加入了一个奖励模型(像Qwen 2和AFM那样)。他们还使用了像Gemma那样的模型平均;然而,这不仅适用于奖励模型,而是涉及所有模型。

                                  Llama 3.1后训练技术总结。

4.4 结论

Llama 3模型保持相当标准,与早期的Llama 2模型类似,但采用了一些有趣的方法。值得注意的是,庞大的15万亿标记训练集使Llama 3与其他模型区别开来。有趣的是,像苹果的AFM模型一样,Llama 3也实施了三阶段预训练过程。

与其他最近的大型语言模型不同,Llama 3没有采用知识蒸馏技术,而是选择了一条更直接的模型开发路径。在后训练中,模型使用了直接偏好优化(DPO)而不是其他模型中流行的更复杂的强化学习策略。总的来说,这种选择很有趣,因为它表明了通过简单(但经过验证的)方法精炼LLM性能的重点。

5. 主要收获

从本文讨论的这四个模型中我们可以学到什么:阿里巴巴的Qwen 2,苹果的基础模型(AFM),谷歌的Gemma 2,以及Meta的Llama 3?

这四个模型在预训练和后训练上采取了略有不同的方法。当然,方法论有所重叠,但没有哪个训练流程是完全相同的。在预训练方面,一个共有的特征似乎是所有方法都使用了多阶段预训练流程,其中一般的核心预训练之后是上下文延长,有时还有高质量的退火步骤。下面的图表再次一览无余地显示了预训练中使用的不同方法。

                                                            预训练所用技术概述

在后训练方面,也没有哪个流程是完全相同的。看来拒绝采样现在已经成为后训练过程中的常见要素。然而,当谈到DPO或RLHF时,还没有共识或偏好(无意中的双关语)。


后训练所用技术概述

因此,总的来说,没有单一的配方,而是有许多路径可以开发出高性能的LLM。

最后,这四个模型的表现大致相当。不幸的是,其中几个模型还没有进入LMSYS和AlpacaEval排行榜,所以我们还没有直接的比较,除了在MMLU和其他多项选择基准上的得分。

背景事件
:近日,优衣库宣布不再使用新疆棉花,这一举措引发了广泛的社会讨论。消费者的反应和舆论的压力,让优衣库的决策迅速影响了市场和品牌形象。类似的,许多系统也面临着需要根据外部事件或状态的变化,做出即时反应的需求。

在软件设计中,
观察者模式(Observer Pattern)
就是为了处理这种“状态变化与反应”的需求。它允许一个对象(主题)发生变化时,通知所有依赖它的对象(观察者),而不需要显式的调用,
即松耦合
地处理不同模块间的动态更新。

今天,我们将结合优衣库棉花事件,来讲解如何使用
观察者模式
来应对复杂业务中的事件驱动响应。

一、观察者模式概述

1.1 观察者模式的定义

观察者模式属于
行为型设计模式
,其核心思想是:
当对象的状态发生改变时,所有依赖于它的对象都会收到通知并自动更新
。这种模式非常适用于当系统中的某一部分状态变化需要同时通知到其他部分的场景。

1.2 观察者模式的组成部分

  • 主题(Subject)
    :状态变化的核心对象,负责管理所有的观察者并通知它们。
  • 观察者(Observer)
    :依赖于主题对象的变化,并作出响应。
  • 通知机制
    :通过注册和触发通知,观察者获得主题对象的状态变化。


优衣库不使用新疆棉
的事件中,
品牌决策
就像
主题对象
,而
消费者、舆论、媒体等
就是观察者。每当品牌作出决策,消费者的情感、舆论的反应都会发生变化,观察者(消费者)的情绪或行为会随之改变。

二、观察者模式与优衣库事件的类比

2.1 事件背景

假设优衣库公司决定不再使用新疆棉,这一决策会触发不同的消费者、股东、媒体等不同的观察者产生反应:

  • 消费者
    可能对这一举措产生负面或正面的情绪,甚至会改变购买行为。
  • 媒体
    可能会通过报道引导舆论。
  • 股东
    可能对这一决策的影响做出反应,调整投资策略。

这些变化是
动态的
,且每当优衣库做出决策时,所有相关的观察者(消费者、媒体等)都会收到通知并做出相应反应。

2.2 在系统设计中的应用

在类似的业务场景中,观察者模式非常有用。通过观察者模式,当主题(如优衣库品牌决策)发生变化时,所有需要响应变化的组件(如消费者、舆论)都会自动收到通知并执行相应的处理逻辑。这种设计使得不同的模块之间保持了松耦合的关系,不需要主题对象知道观察者的具体细节。

三、观察者模式的实现

接下来,我们通过代码来实现一个基于观察者模式的简单示例,模拟优衣库品牌决策引发的消费者反应。

3.1 代码示例:实现观察者模式

// 主题类:优衣库品牌决策
import java.util.ArrayList;
import java.util.List;

// 主题接口
interface BrandDecision {
    void addObserver(ConsumerObserver observer);
    void removeObserver(ConsumerObserver observer);
    void notifyObservers();
}

// 具体主题:优衣库的品牌决策
class UniqloBrandDecision implements BrandDecision {
    private List<ConsumerObserver> observers = new ArrayList<>();
    private String decision;

    @Override
    public void addObserver(ConsumerObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(ConsumerObserver observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for (ConsumerObserver observer : observers) {
            observer.update(decision);
        }
    }

    public void makeDecision(String decision) {
        this.decision = decision;
        System.out.println("优衣库品牌做出决策: " + decision);
        notifyObservers();
    }
}

// 观察者接口:消费者反应
interface ConsumerObserver {
    void update(String decision);
}

// 具体观察者:消费者
class Consumer implements ConsumerObserver {
    private String name;

    public Consumer(String name) {
        this.name = name;
    }

    @Override
    public void update(String decision) {
        System.out.println(name + " 收到决策通知,决定如何行动: " + decision);
        if (decision.contains("不使用新疆棉")) {
            System.out.println(name + " 表示失望,可能不再购买!");
        } else {
            System.out.println(name + " 觉得决策不错,继续购买!");
        }
    }
}

3.2 代码分析

在上面的代码中:

  • UniqloBrandDecision
    类是主题,它包含了消费者的观察者列表。当优衣库作出决策时,会调用
    makeDecision()
    方法,通知所有的观察者(消费者)进行反应。
  • Consumer
    类是具体的观察者,当品牌决策改变时,它会根据决策内容做出不同的反应。

3.3 客户端代码:模拟消费者反应

public class ObserverPatternExample {
    public static void main(String[] args) {
        // 创建品牌决策主题
        UniqloBrandDecision uniqloDecision = new UniqloBrandDecision();

        // 创建消费者观察者
        Consumer consumer1 = new Consumer("消费者A");
        Consumer consumer2 = new Consumer("消费者B");

        // 消费者注册到品牌决策
        uniqloDecision.addObserver(consumer1);
        uniqloDecision.addObserver(consumer2);

        // 优衣库做出决策
        uniqloDecision.makeDecision("优衣库决定不再使用新疆棉,改用其他地区棉花");

        // 优衣库做出新的决策
        uniqloDecision.makeDecision("优衣库决定继续使用新疆棉");
    }
}

3.4 输出结果

优衣库品牌做出决策: 优衣库决定不再使用新疆棉,改用其他地区棉花
消费者A 收到决策通知,决定如何行动: 优衣库决定不再使用新疆棉,改用其他地区棉花
消费者A 表示失望,可能不再购买!
消费者B 收到决策通知,决定如何行动: 优衣库决定不再使用新疆棉,改用其他地区棉花
消费者B 表示失望,可能不再购买!
优衣库品牌做出决策: 优衣库决定继续使用新疆棉
消费者A 收到决策通知,决定如何行动: 优衣库决定继续使用新疆棉
消费者A 觉得决策不错,继续购买!
消费者B 收到决策通知,决定如何行动: 优衣库决定继续使用新疆棉
消费者B 觉得决策不错,继续购买!

四、观察者模式的优势与应用

4.1 优点

  • 松耦合
    :主题和观察者之间没有紧密的耦合关系。主题只知道它需要通知观察者,而不关心观察者的具体实现。
  • 动态响应
    :观察者可以根据主题的状态变化动态调整自己的行为,非常适合事件驱动的系统。
  • 易于扩展
    :添加新的观察者(如新消费者)非常方便,不需要改变现有的代码。

4.2 缺点

  • 通知开销
    :如果观察者数量很多,通知机制可能会成为性能瓶颈。
  • 依赖顺序
    :观察者的顺序可能影响到通知的处理流程,某些场景下可能需要额外的控制。

五、总结

通过优衣库不使用新疆棉事件,我们可以清晰地看到观察者模式在应对复杂决策和动态反应中的优势。当品牌作出决策时,消费者和舆论等多个观察者会收到通知并做出反应,
无缝的事件驱动机制
让这种决策的传播变得高效且灵活。

如果你的系统中涉及到多个模块需要对某一变化做出响应,观察者模式无疑是一个非常合适的解决方案。它能够帮助你保持系统的松耦合,快速响应变化,同时确保灵活的扩展性。


希望这篇文章能够帮助你理解如何通过
观察者模式
应对复杂的业务决策和系统响应!如果你对其他设计模式的应用感
作者:
代老师的编程课
出处:
https://zthinker.com/
如果你喜欢本文,请长按二维码,关注
Java码界探秘
.
代老师的编程课