2024年11月

书接上回,今天继续和大家享一些关于枚举操作相关的常用扩展方法。

今天主要分享通过枚举值转换成枚举、枚举名称以及枚举描述相关实现。

我们首先修改一下上一篇定义用来测试的正常枚举,新增一个枚举项,代码如下:

//正常枚举
internal enum StatusEnum
{
    [Description("正常")]
    Normal = 0,
    [Description("待机")]
    Standby = 1,
    [Description("离线")]
    Offline = 2,
    Online = 3,
    Fault = 4,
}

01
、根据枚举值转换成枚举

该方法接收枚举值作为参数,并转为对应枚举,转换失败则返回空。

枚举类Enum中自带了两种转换方法,其一上篇文章使用过即Parse,这个方法可以接收string或Type类型作为参数,其二为ToObject方法,接收参数为整数类型。

因为枚举值本身就是整数类型,因此我们选择ToObject方法作为最终实现,这样就避免使用Parse方法时还需要把整数类型参数进行转换。

同时我们通过上图可以发现枚举值可能的类型有uint、ulong、ushort、sbyte、long、int、byte、short八种情况。

因此下面我们以int类型作为示例,进行说明,但同时考虑到后面通用性、扩展性,我们再封装一个公共的泛型实现可以做到支持上面八种类型。因此本方法会调用一个内部私有方法,具体如下:

//根据枚举值转换成枚举,转换失败则返回空
public static T? ToEnumByValue<T>(this int value) where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    return ToEnumByValue<int, T>(value);
}

而内部私有方法即通过泛型实现对多种类型支持,我们先看代码实现再详细讲解,具体代码如下:

//根据枚举值转换成枚举,转换失败则返回空
private static TEnum? ToEnumByValue<TSource, TEnum>(this TSource value)
    where TSource : struct
    where TEnum : struct, Enum
{
    //检查整数值是否是有效的枚举值并且是否是有效位标志枚举组合项
    if (!Enum.IsDefined(typeof(TEnum), value) && !IsValidFlagsMask<TSource, TEnum>(value))
    {
        //非法数据则返回空
        return default;
    }
    //有效枚举值则进行转换
    return (TEnum)Enum.ToObject(typeof(TEnum), value);
}

该方法首先验证参数合法性,验证通过直接使用ToObject方法进行转换。

参数验证首先通过Enum.IsDefined方法校验参数是否是有效的枚举项,这是因为无论是ToObject方法还是Parse方法对于整数类型参数都是可以转换成功的,无论这个参数是否是枚举中的项,因此我们需要首先排查掉非枚举中的项。

而该方法中IsValidFlagsMask方法主要是针对位标志枚举组合情况,位标志枚举特性导致即使我们枚举项中没有定义相关项,但是可以通过组合得到而且是合法的,因此我们需要对位标志枚举单独处理,具体实现代码如下:

//存储枚举是否为位标志枚举
private static readonly ConcurrentDictionary<Type, bool> _flags = new();
//存储枚举对应掩码值
private static readonly ConcurrentDictionary<Type, long> _flagsMasks = new();
private static bool IsValidFlagsMask<TSource, TEnum>(TSource source)
    where TSource : struct
    where TEnum : struct, Enum
{
    var type = typeof(TEnum);
    //判断是否为位标志枚举,如果有缓存直接获取,否则计算后存入缓存再返回
    var isFlags = _flags.GetOrAdd(type, (key) =>
    {
        //检查枚举类型是否有Flags特性
        return Attribute.IsDefined(key, typeof(FlagsAttribute));
    });
    //如果不是位标志枚举则返回false
    if (!isFlags)
    {
        return false;
    }
    //获取枚举掩码,如果有缓存直接获取,否则计算后存入缓存再返回
    var mask = _flagsMasks.GetOrAdd(type, (key) =>
    {
        //初始化存储掩码变量
        var mask = 0L;
        //获取枚举所有值
        var values = Enum.GetValues(key);
        //遍历所有枚举值,通过位或运算合并所有枚举值
        foreach (Enum enumValue in values)
        {
            //将枚举值转为long类型
            var valueLong = Convert.ToInt64(enumValue);
            // 过滤掉负数或无效的值,规范的位标志枚举应该都为非负数
            if (valueLong >= 0)
            {
                //合并枚举值至mask
                mask |= valueLong;
            }
        }
        //返回包含所有枚举值的枚举掩码
        return mask;
    });
    var value = Convert.ToInt64(source);
    //使用待验证值value和枚举掩码取反做与运算
    //结果等于0表示value为有效枚举值
    return (value & ~mask) == 0;
}

该方法首先是判断当前枚举是否是位标志枚举即枚举是否带有Flags特性,可以通过Attribute.IsDefined实现,考虑到性能问题,因此我们把枚举是否为位标志枚举存入缓存中,当下次使用时就不必再次判断了。

如果当前枚举不是位标志枚举则之间返回false。

如果是位标志枚举则进入关键点了,如何判断一个值是否为一组值或一组值任意组合里面的一个?

这里用到了位掩码技术,通过按位或对所有枚举项进行标记,达到合并所有枚举项的目的,同时还包括可能的组合情况。

这里存储掩码的变量定义为long类型,因为我们需要兼容上文提到的八种整数类型。同时一个符合规范的位标志枚举设计枚举值是不会出现负数的因此也需要过滤掉。

同时考虑到性能问题,也需要把每个枚举对于的枚举掩码记录到缓存中方便下次使用。

拿到枚举掩码后我们需要对其进行取反,即表示所有符合要求的值,此值再与待验证参数做按位与操作,如果不为0表示待验证才是为无效枚举值,否则为有效枚举值。

关于位操作我们后面找机会再单独详解讲解其中原理和奥秘。

讲解完整个实现过程我们还需要对该方法进行详细的单元测试,具体分为以下几种情况:

(1) 正常枚举值,成功转换成枚举;

(2) 不存在的枚举值,但是可以通过枚举项按位或合并得到,返回空;

(3) 不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空;

(4) 正常位标志枚举值,成功转换成枚举;

(5) 不存在的枚举值,但是可以通过枚举项按位或合并得到,成功转换成枚举;

(6) 不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空;

具体实现代码如下:

[Fact]
public void ToEnumByValue()
{
    //正常枚举值,成功转换成枚举
    var status = 1.ToEnumByValue<StatusEnum>();
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举值,但是可以通过枚举项按位或合并得到,返回空
    var isStatusNull = 5.ToEnumByValue<StatusEnum>();
    Assert.Null(isStatusNull);
    //不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空
    var isStatusNull1 = 8.ToEnumByValue<StatusEnum>();
    Assert.Null(isStatusNull1);
    //正常位标志枚举值,成功转换成枚举
    var flags = 3.ToEnumByValue<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.HttpAndUdp, flags);
    //不存在的枚举值,但是可以通过枚举项按位或合并得到,成功转换成枚举
    var flagsGroup = 5.ToEnumByValue<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.Http | TypeFlagsEnum.Tcp, flagsGroup);
    //不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空
    var isFlagsNull = 8.ToEnumByValue<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

02
、根据枚举值转换成枚举或默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举,具体代码如下:

//根据枚举值转换成枚举,转换失败则返回默认枚举
public static T? ToEnumOrDefaultByValue<T>(this int value, T defaultValue) 
    where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回枚举
        return result.Value;
    }
    //转换失败则返回默认枚举
    return defaultValue;
}

然后我们进行一个简单单元测试,代码如下:

[Fact]
public void ToEnumOrDefaultByValue()
{
    //正常枚举值,成功转换成枚举
    var status = 1.ToEnumOrDefaultByValue(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举值,返回指定默认枚举
    var statusDefault = 5.ToEnumOrDefaultByValue(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Offline, statusDefault);
}

03
、根据枚举值转换成枚举名称

该方法接收枚举值作为参数,并转为对应枚举名称,转换失败则返回空。

实现则是通过根据枚举值转换成枚举方法获得枚举,然后通过枚举获取枚举名称,具体代码如下:

//根据枚举值转换成枚举名称,转换失败则返回空
public static string? ToEnumNameByValue<T>(this int value) where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回枚举名称
        return result.Value.ToString();
    }
    //转换失败则返回空
    return default;
}

我们进行如下单元测试:

[Fact]
public void ToEnumNameByValue()
{
    //正常枚举值,成功转换成枚举名称
    var status = 1.ToEnumNameByValue<StatusEnum>();
    Assert.Equal("Standby", status);
    //不存在的枚举值,返回空
    var isStatusNull = 10.ToEnumNameByValue<StatusEnum>();
    Assert.Null(isStatusNull);
    //正常位标志枚举值,成功转换成枚举名称
    var flags = 3.ToEnumNameByValue<TypeFlagsEnum>();
    Assert.Equal("HttpAndUdp", flags);
    //不存在的位标志枚举值,返回空
    var isFlagsNull = 20.ToEnumNameByValue<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

04
、根据枚举值转换成枚举名称默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举名称,具体代码如下:

//根据枚举值转换成枚举名称,转换失败则返回默认枚举名称
public static string? ToEnumNameOrDefaultByValue<T>(this int value, string defaultValue) 
    where T : struct, Enum
{
    //调用根据枚举值转换成枚举名称方法
    var result = value.ToEnumNameByValue<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        //返回枚举名称
        return result;
    }
    //转换失败则返回默认枚举名称
    return defaultValue;
}

进行简单的单元测试,具体代码如下:

[Fact]
public void ToEnumNameOrDefaultByValue()
{
    //正常枚举值,成功转换成枚举名称
    var status = 1.ToEnumNameOrDefaultByValue<StatusEnum>("离线");
    Assert.Equal("Standby", status);
    //不存在的枚举名值,返回指定默认枚举名称
    var statusDefault = 12.ToEnumNameOrDefaultByValue<StatusEnum>("离线");
    Assert.Equal("离线", statusDefault);
}

05
、根据枚举值转换成枚举描述

该方法接收枚举值作为参数,并转为对应枚举名称,转换失败则返回空。

实现则是通过根据枚举值转换成枚举方法获得枚举,然后通过枚举获取枚举描述,具体代码如下:

//根据枚举值转换成枚举描述,转换失败则返回空
public static string? ToEnumDescByValue<T>(this int value) where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回枚举描述
        return result.Value.ToEnumDesc();
    }
    //转换失败则返回空
    return default;
}

单元测试如下:

[Fact]
public void ToEnumDescByValue()
{
    //正常位标志枚举值,成功转换成枚举描述
    var flags = 3.ToEnumDescByValue<TypeFlagsEnum>();
    Assert.Equal("Http协议,Udp协议", flags);
    //正常的位标志枚举值,组合项不存在,成功转换成枚举描述
    var flagsGroup1 = 5.ToEnumDescByValue<TypeFlagsEnum>();
    Assert.Equal("Http协议,Tcp协议", flagsGroup1);
}

06
、根据枚举值转换成枚举描述默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举描述,具体代码如下:

//根据枚举值转换成枚举描述,转换失败则返回默认枚举描述
public static string? ToEnumDescOrDefaultByValue<T>(this int value, string defaultValue) 
    where T : struct, Enum
{
    //调用根据枚举值转换成枚举描述方法
    var result = value.ToEnumDescByValue<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        //返回枚举描述
        return result;
    }
    //转换失败则返回默认枚举描述
    return defaultValue;
}

单元测试代码如下:

[Fact]
public void ToEnumDescOrDefaultByValue()
{
    //正常枚举值,成功转换成枚举描述
    var status = 1.ToEnumDescOrDefaultByValue<StatusEnum>("测试");
    Assert.Equal("待机", status);
    //不存在的枚举值,返回指定默认枚举描述
    var statusDefault = 11.ToEnumDescOrDefaultByValue<StatusEnum>("测试");
    Assert.Equal("测试", statusDefault);
}

稍晚些时候我会把库上传至Nuget,大家可以直接使用Ideal.Core.Common。


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

概述

  • undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的
    原子性
    ,主要用于事务回滚和 MVCC。
  • redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的
    持久性
    ,主要用于掉电等故障恢复;
  • binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;

回滚日志(undo log)

作用

  • 保存了事务发生之前的数据的一个版本,可以用于回滚,保障原子性
  • 实现多版本并发控制下的读(MVCC)的关键因素之一,也即非锁定读,MVCC通过Read View + undolog的版本链实现,可以具体看
    MVCC的快照读

内容

逻辑格式的日志,在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log 的。

每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:


  • 插入insert
    一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的
    记录删掉delete
    就好了;

  • 删除delete
    一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的
    记录插入insert
    到表中就好了;

  • 更新
    一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列
    更新为旧值
    就好了。

什么时候产生

事务开始之前
,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。同时undo 也会产生 redo 来保证undo log的可靠性。

什么时候刷盘

undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。产生undo日志的时候,同样会伴随类似于保护事务持久化机制的redolog的产生。

buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的,具体看下面内容。

重做日志(redo log)

作用

  • 确保事务的持久性。
    • 为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。也就是说, redo log 是为了防止 Buffer Pool 中的脏页丢失而设计的。
    • 在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
  • 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。

内容

物理格式的日志,记录的是物理数据页面的修改的信息,其 redo log 是顺序写入redo log file 的物理文件中去的。同时,在内存修改 Undo log 后,也需要记录undo log对应的 redo log。

redo log 和 undo log 区别:

  • redo log 记录了此次事务
    完成后
    的数据状态,记录的是更新之后的值;
  • undo log 记录了此次事务
    开始前
    的数据状态,记录的是更新之前的值;

什么时候产生

事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。

事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务,如下图:

redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?

写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。

什么时候刷盘

实际上, 执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。

redo log有一个缓存区 Innodb_log_buffer,Innodb_log_buffer 的默认大小为 16M,每当产生一条 redo log 时,会先写入到 redo log buffer,后续再持久化到磁盘。

然后会通过以下三种方式将innodb log buffer的日志刷新到磁盘:

  • MySQL 正常关闭时;
  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘。

因此redo log buffer的写盘,并不一定是随着事务的提交才写入redo log文件的,而是随着事务的开始,逐步开始的。

即使某个事务还没有提交,Innodb存储引擎仍然每秒会将redo log buffer刷新到redo log文件。

这一点是必须要知道的,因为这可以很好地解释再大的事务的提交(commit)的时间也是很短暂的。

redolog的文件

两个 redo 日志的文件名叫 :ib_logfile0 和 ib_logfile1。

redo log文件组是以循环写的方式工作的, InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件;相当于一个环形。

  • write pos 和 checkpoint 的移动都是顺时针方向;
  • write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;
  • check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录;

因此,如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞

二进制日志(binlog)

作用

  • 用于复制,在主从复制中,从库利用主库上的binlog进行重放,实现主从同步。
  • 用于数据库的基于时间点的还原,即备份恢复

内容

binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:

  • STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
  • ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
  • MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;

注意:不同的日志类型在主从复制下除了有动态函数的问题,同样对对更新时间也有影响。一般来说,数据库中的update_time都会设置成ON UPDATE CURRENT_TIMESTAMP,即自动更新时间戳列。在主从复制下,
如果日志格式类型是STATEMENT,由于记录的是sql语句,在salve端是进行语句重放,那么更新时间也是重放时的时间,此时slave会有时间延迟的问题;
如果日志格式类型是ROW,这是记录行数据最终被修改成什么样了,这种从库的数据是与主服务器完全一致的。

什么时候产生

事务
提交的时候
,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。

binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。

这里与redo log很明显的差异就是binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

也就是说,如果不小心整个数据库的数据被删除了,只能使用 bin log 文件恢复数据。因为redo log循环写会擦除数据。

主从复制的实现

MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。

这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。

MySQL 集群的主从复制过程如下:

  • 写入 Binlog:MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
  • 同步 Binlog:从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
  • 回放 Binlog:从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

什么时候刷盘

在刷盘时机上与redolog不一样,redolog即使事务没提交,也可以每隔1秒就刷盘。但是一个事务的 binlog 是不能被拆开的,因此无论这个事务有多大(比如有很多条语句),也要保证一次性写入。如果一个事务的 binlog 被拆开的时候,在备库执行就会被当做多个事务分段自行,这样就破坏了原子性,是有问题的。

bin log日志与redo log类似,也有对应的缓存,叫 binlog cache。事务提交的时候,再把 binlog cache 写到 binlog 文件中。

  • 图中的 write,指的就是指把日志写入到 binlog 文件,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。
  • 图中的 fsync,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。

MySQL提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:

  • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
  • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;
  • sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

显然,在MySQL中系统默认的设置是 sync_binlog = 0,也就是不做任何强制性的磁盘刷新指令,这时候的性能是最好的,但是风险也是最大的。因为一旦主机发生异常重启,还没持久化到磁盘的数据就会丢失。

而当 sync_binlog 设置为 1 的时候,是最安全但是性能损耗最大的设置。因为当设置为 1 的时候,即使主机发生异常重启,最多丢失一个事务的 binlog,而已经持久化到磁盘的数据就不会有影响,不过就是对写入性能影响太大。

如果能容少量事务的 binlog 日志丢失的风险,为了提高写入的性能,一般会 sync_binlog 设置为 100~1000 中的某个数值。

两阶段提交

事务提交后,redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致。如下:

  1. 如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。那么机器重启后,这台机器会通过redo log恢复数据,但是这个时候binlog并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
  2. 如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。由于 redo log 还没写,崩溃恢复以后这个事务无效,而 binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,那么就与主库的值不一致性;

两阶段提交把单个事务的提交拆分成了 2 个阶段,分别是「准备(Prepare)阶段」和「提交(Commit)阶段」

具体过程


事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;

总的来说就是,事务提交后,redo log变成prepare 阶段,再写入binlog,返回成功后redo log 进入commit 阶段。

总结三个日志的具体流程

当优化器分析出成本最小的执行计划后,执行器就按照执行计划开始进行更新操作。

具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:

  1. 检查在不在buffer Pool。 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
  2. 检查是否已经是要更新的值。执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
    • 如果一样的话就不进行后续更新流程;
    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
  3. 开启事务,记录undo log,并记录修改undo log对应的redo log:开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
  4. 标记为脏页,并写入redo log:InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
  5. 至此,一条记录更新完了。
  6. 记录binlog:在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
  7. 事务提交,redo log和binlog刷盘。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

教程名称:使用 C# 入门深度学习

作者:痴者工良

地址:

https://torch.whuanle.cn

线性代数

推荐书籍

大家都知道学习 Pytorch 或 AI 需要一定的数学基础,当然也不需要太高,只需要掌握一些基础知识和求解方法,常见需要的数学基础有线性代数、微积分、概率论等,由于高等数学课程里面同时包含了线性代数和微积分的知识,因此读者只需要学习高等数学、概率论两门课程即可。数学不用看得太深,这样太花时间了,能理解意思就行。


首先推荐以下两本书,无论是否已经忘记了初高中数学知识,对于数学基础薄弱的读者来说,都可以看。

  • 《普林斯顿微积分读本》

  • 《普林斯顿概率论读本》


国内的书主要是一些教材,学习难度会大一些,不过完整看完可以提升数学水平,例如同济大学出版的《高等数学》上下册、《概率论与数理统计》,不过国内的这些教材主要为了刷题解题、考研考试,可能不太适合读者,而且学习起来的时间也太长了。


接着是推荐《深度学习中的数学》,作者是涌井良幸和涌井贞美,对于入门的读者来说上手难度也大一些,不那么容易看得进去,读者可以在看完本文之后再去阅读这本经典书,相信会更加容易读懂。

另外,千万不要用微信读书这些工具看数学书,排版乱七八糟的,数学公式是各种抠图,数学符号也是用图片拼凑的,再比如公式里面中文英文符号都不分。

建议直接买实体书,容易深度思考,数学要多答题解题才行。就算买来吃灰,放在书架也可以装逼呀。买吧。


本文虽然不要求读者数学基础,但是还是需要知道一些数学符号的,例如求和∑ 、集合交并∩∪等,这些在本文中不会再赘述,读者不理解的时候需要自行搜索资料。


基础知识

标量、向量、矩阵

笔者只能给出大体的概念,至于数学上的具体定义,这里就不展开了。

标量(scalar):只有大小没有方向的数值,例如体重、身高。

向量(vector):既有大小也有方向的数值,可以用行或列来表示。

矩阵(matrix):由多行多列的向量组成。

张量(Tensor):在 Pytorch 中,torch.Tensor 类型数据结构就是张量,结构跟数组或矩阵相似。


  • Tensor:是PyTorch中的基本数据类型,可以理解为多维数组。 Tensor可以用来表示数据集、模型参数和模型输出等。
  • Scalar:是一个特殊类型的Tensor,只有一维。 Scalar用来表示标量值,如学习率、损失值等。
  • Vector:是一个特殊类型的Tensor,有一维或两维。 Vector用来表示向量值,如梯度、特征值等。
  • Matrix:是一个特殊类型的Tensor,有两维。 Matrix用来表示矩阵值,如权重矩阵、输出矩阵等。


比如说 1.0、2 这些都是标量,在各种编程语言中都以基础数据类型提供了支持,例如 C# 的基元类型。


下面将标量转换为 torch.Tensor 类型。

var x = torch.tensor(1.0);
var y = torch.tensor(2);

x.print_csharp();
y.print_csharp();
[], type = Float64, device = cpu, value = 1
[], type = Int32, device = cpu, value = 2


将数组转换为 torch.Tensor 类型:

var data = new int[ , ]{ {1, 2}, { 3, 4}};
var x_data = torch.tensor(data);

x_data.print_csharp();


由于上一章已经讲解了很多数组的创建方式,因此这里不再赘述。


Pytorch 的一些数学函数

Pytorch 通过 torch.Tensor 表示各种数据类型,torch.Tensor 提供超过 100 多种的张量操作,例如算术运算、线性代数、矩阵操作、采样等。

由于篇幅有限,这里就不单独给出,读者请自行参考以下资料:

https://pytorch.org/docs/stable/torch.html

https://pytorch.ac.cn/docs/stable/torch.html


线性代数


向量

向量的概念

在研究力学、物理学等工程应用领域中会碰到两类的量,一类完全由
数值的大小
决定,例如温度、时间、面积、体积、密度、质量等,称为
数量

标量
,另一类的量,
只知道数值的大小还不能完全确定所描述量
,例如加速度、速度等,这些量除了大小还有方向,称为向量。


在平面坐标轴上有两点
\(A(x_{1},y_{1})\)

\(B(x_{2},y_{2})\)
,以 A 为起点 、B 为终点的线段被称为被称为有向线段,其既有大小也有方向,使用 $\overrightarrow{AB} $ 表示,使用坐标表示为
\((x_{2}-x_{1},y_{2}-y_{1})\)
,如果不强调方向,也可以使用 $\alpha $ 等符号进行简记。

image-20241108071154361


A、B 之间的距离称为向量的模,使用 | $\overrightarrow{AB} $ | 或 | $\overrightarrow{BA} $ | 或 | $\alpha $ | 表示。

平面中的向量,其距离公式是:

\[| \overrightarrow{AB} | = \sqrt{(x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2}
\]


其实原理也很简单,根据勾股定理,AB 的平方等于两个直角边长平方之和,所以:

\[| \overrightarrow{AB} | ^2 = (x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2
\]


image-20241108212312023


去平方就是:

\[| \overrightarrow{AB} | = \sqrt{(x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2}
\]


如下图所示,其两点间的距离:

\[ | \overrightarrow{AB} | = \sqrt{(4-1)^{2} + (4-1)^2} = \sqrt{18} = 3\sqrt{2} = 4.242640687119285
\]


image-20241108071828663


使用 C# 计算向量的模,结果如下

var A = torch.from_array(new[] { 1.0, 1.0 });
var B = torch.from_array(new[] { 4.0, 4.0 });
var a = B - A;

var norm = torch.norm(a);
norm.print_csharp();
[], type = Float64, device = cpu, value = 4.2426

注意,计算向量的模只能使用浮点型数据,不能使用 int、long 这些整型。


同理,对于三维空间中的两点
\(A(x_{1},y_{1},z_{1})\)

\(B(x_{2},y_{2},z_{2})\)
,距离公式是:

\[| \overrightarrow{AB} | = \sqrt{(x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2 + (z_{2}-z_{1})^2}
\]


向量的加减乘除法

向量的加法很简单,坐标相加即可。

如图所示,平面中有三点 A(1,1)、B(3,5)、C(6,4)。

image-20241108205142069


得到三个向量分别为:$\overrightarrow{AB} (2,4)
\(、\)
\overrightarrow{BC} (3,-1)
\(、\)
\overrightarrow{AC} (5,3) $


根据数学上向量的加法可知,$\overrightarrow{AB} $ + $\overrightarrow{BC} $ = $\overrightarrow{AC} $

var B = torch.from_array(new[] { 2.0, 4.0 });
var A = torch.from_array(new[] { 3.0, -1.0 });
var a = A + B;

a.print_csharp();
[2], type = Float64, device = cpu, value = double [] {5, 3}


同理,在 Pytorch 中,向量减法也是两个 torch.Tensor 类型相减即可。

推广到三维空间,计算方法也是一样的。

var B = torch.from_array(new[] { 2.0, 3.0, 4.0 });
var A = torch.from_array(new[] { 3.0, 4.0, 5.0 });
var a = B - A;

a.print_csharp();
[3], type = Float64, device = cpu, value = double [] {-1, -1, -1}


另外,向量乘以或除以一个标量,直接运算即可,如 $ \overrightarrow{AB} (2,4) $,则 $ 3 * \overrightarrow{AB} (2,4) $ = (6,12)。


向量的投影

如图所示, $\overrightarrow{AB} (2,4) $ 是平面上的向量,如果我们要计算向量在 x、y 上的投影是很简单的,例如在 x 轴上的投影是 2,因为 A 点的 x 坐标是 1,B 点的 x 坐标是 3,所以 3 - 1 = 2 为 $\overrightarrow{AB} (2,4) $ 在 x 轴上的投影,5 - 1 = 4 是在 y 轴上的投影。

image-20241108211302187


在数学上使用
\(Projx(u)\)
表示向量 u 在 x 上的投影,同理
\(Projy(u)\)
是 u 在 y 上的投影。

如果使用三角函数,我们可以这样计算向量在各个轴上的投影。

\[Projx(u) = |\overrightarrow{AB}| \cos \alpha = |\overrightarrow{AC}|
\]

\[Projy(u) = |\overrightarrow{AB}| \sin \alpha = |\overrightarrow{BC}|
\]


AC、BC 长度是 4,根据勾股定理得出 AB 长度是 $4\sqrt{2} $,由于
\(cos \frac{\pi }{2} = \frac{\sqrt{2}} {2}\)
,所以
\(Projx(u) = 4\)

image-20241108212445350


那么在平面中,我们已知向量的坐标,求向量与 x 、y 轴的夹角,可以这样求。

\[\cos \alpha = \frac{x}{|v|}
\]

\[\sin \alpha = \frac{y}{|v|}
\]


例如上图中 $\overrightarrow{AB} (4,4) $,x 和 y 都是 4,其中
\(|v| = 4\sqrt{2}\)
,所以
\(\cos \alpha = \frac{4}{4\sqrt{2}} = \frac{\sqrt{2}}{2}\)


从 x、y 轴推广到平面中任意两个向量
\(\alpha\)

\(\beta\)
,求其夹角
\(\varphi\)
的公式为:

\[\cos \varphi = \frac{\alpha \cdot \beta}{|\alpha|\cdot|\beta|}
\]


继续按下图所示,计算
\(\overrightarrow{AB}\)

\(\overrightarrow{AC}\)
之间的夹角,很明显,我们按经验直接可以得出夹角
\(\varphi\)
是 45° 。

image-20241108221035111


但是如果我们要通过投影方式计算出来,则根据 $ \frac{\alpha \cdot \beta}{|\alpha|\cdot|\beta|} $ ,是 C# 计算如下。

var AB = torch.from_array(new[] { 4.0, 4.0 });
var AC = torch.from_array(new[] { 4.0, 0.0 });

// 点积
var dot = torch.dot(AB, AC);

// 求每个向量的模
var ab = torch.norm(AB);
var ac = torch.norm(AC);

// 求出 cosφ 的值
var cos = dot / (ab * ac);
cos.print_csharp();

// 使用 torch.acos 计算夹角 (以弧度为单位)
var theta = torch.acos(cos);

// 将弧度转换为角度
var theta_degrees = torch.rad2deg(theta);
theta_degrees.print_csharp();
[], type = Float64, device = cpu, value = 0.70711
[], type = Float64, device = cpu, value = 45

image-20241108221229577


柯西-施瓦茨不等式

\(a\)

\(b\)
是两个向量,根据前面学到的投影和夹角知识,我们可以将以下公式进行转换。

\[\cos \varphi = \frac{\alpha \cdot \beta}{|\alpha|\cdot|\beta|}
\]

\[\alpha \cdot \beta = |\alpha|\cdot|\beta| \cos \varphi
\]

由于
\(-1 \le \cos \varphi \le 1\)
,所以:

\[- |\alpha|\cdot|\beta| \le \alpha \cdot \beta \le |\alpha|\cdot|\beta|
\]


这个就是 柯西-施瓦茨不等式。


也就是说,当两个向量的夹角最小时,两个向量的方向相同(角度为0),此时两个向量的乘积达到最大值,角度越大,乘积越小。在深度学习中,可以将两个向量的方向表示为相似程度,例如向量数据库检索文档时,可以算法计算出向量,然后根据相似程度查找最优的文档信息。

image-20241112112037795


向量的点积

点积即向量的数量积,点积、数量积、内积,都是同一个东西。

两个向量的数量积是标量,即一个数值,而向量积是不同的东西,这里只说明数量积。

数量积称为两个向量的数乘,而向量积才是两个向量的乘法。

向量的数乘公式如下:

\[a\cdot b=\displaystyle\sum_{i=1}^{n} a_{i} b_{i}=a_{1} b_{1}+a_{2} b_{2}+...+a_{n} b_{n}
\]


加上前面学习投影时列出的公式,如果可以知道向量的模和夹角,我们也可以这样求向量的点积:

\[\alpha \cdot \beta = |\alpha|\cdot|\beta| \cos \varphi
\]


例如 $\overrightarrow{AB} (2,4)
\(、\)
\overrightarrow{BC} (3,-1) $ 两个向量,如下图所示。

image-20241108205142069

计算其点积如下:

var B = torch.from_array(new[] { 2.0, 4.0 });
var A = torch.from_array(new[] { 3.0, -1.0 });

var dot = torch.dot(A, B);

dot.print_csharp();
[], type = Float64, device = cpu, value = 2


读者可以试试根据点积结果计算出
\(\angle ABC\)
的角度。


向量积

在画坐标轴时,我们默认轴上每个点间距都是 1,此时 x、y、z 上的单位向量都是 1,如果一个向量的模是 1,那么这个向量就是单位向量,所以单位向量可以有无数个。

image-20241113004516264


在数学中,我们往往会有很多未知数,此时我们使用
\(i\)

\(j\)

\(k\)
分别表示与 x、y、z 轴上正向一致的三个单位向量,
在数学和物理中,单位向量通常用于表示方向而不关心其大小
。不理解这句话也没关系,忽略。


在不关心向量大小的情况下,我们使用单位向量可以这样表示两个向量:

\[a = x_{1}i+y_{1}j+z_{1}k = (x_{1}, y_{1}, z_{1})
\]

\[b = x_{2}i+y_{2}j+z_{2}k = (x_{2}, y_{2}, z_{2})
\]


在三维空间中,
\(i\)

\(j\)

\(k\)
分别表示三个轴方向的单位向量。

  • \(i\)
    表示沿 x 轴方向的单位向量。
  • \(j\)
    表示沿 y 轴方向的单位向量。
  • \(k\)
    表示沿 z 轴方向的单位向量。

这种方式表示 a 在 x 轴上有
\(x_{1}\)
个单位,在 y 轴上有
\(y_{1}\)
个单位,在 z 轴上有
\(z_{1}\)
个单位。

一般来说,提供这种向量表示法,我们并不关心向量的大小,我们只关心其方向,如下图所示。

image-20241108223336564

现在我们来求解一个问题,在空间中找到跟 $\overrightarrow{AB}
\(、\)
\overrightarrow{BC} $ 同时垂直的向量,例如下图的 $\overrightarrow{AD} $,很明显,这样的向量不止一个,有无数个,所以我们这个时候要了解什么是法向量和单位向量。

image-20241113005446796

法向量是一个与平面垂直的向量(这里不涉及曲面、曲线这些),要找出法向量也很简单,有两种方法,一种是坐标表示:

\[a \times b =
\begin{vmatrix}
&i &j &k \\
&x_{1} &y_{1} &z_{1} \\
&x_{2} &y_{2} &z_{2}
\end{vmatrix} = (y_{1}z_{2}-z_{1}y_{2})i - (x_{1}z_{2}-z_{1}x_{2})j + (x_{1}y_{2}-y_{1}x_{2})k
\]


这样记起来有些困难,我们可以这样看,容易记得。

\[a \times b =
\begin{vmatrix}
&i &j &k \\
&x_{1} &y_{1} &z_{1} \\
&x_{2} &y_{2} &z_{2}
\end{vmatrix} = (y_{1}z_{2}-z_{1}y_{2})i + (z_{1}x_{2}-x_{1}z_{2})j + (x_{1}y_{2}-y_{1}x_{2})k
\]


那么法向量
\(n\)

\(x = (y_{1}{z2} -z_{1}y_{2})\)
,y、z 轴同理,就不给出了,x、y、z 分别就是 i、j、k 前面的一块符号公式,所以法向量为:

\[n(y_{1}z_{2}-z_{1}y_{2},z_{1}x_{2}-x_{1}z_{2},x_{1}y_{2}-y_{1}x_{2})
\]


任何一条下式满足的向量,都跟
\(a\)

\(b\)
组成的平面垂直。

\[c = (y_{1}z_{2}-z_{1}y_{2})i + (z_{1}x_{2}-x_{1}z_{2})j + (x_{1}y_{2}-y_{1}x_{2})k
\]


例题如下。

求与
\(a = 3i - 2j + 4k\)

\(b = i + j - 2k\)
都垂直的法向量 。

首先提取
\(a\)
在每个坐标轴上的分量
\((3,-2,4)\)
,b 的分量为
\((1,1,-2)\)

则:

\[a \times b =
\begin{vmatrix}
&i &j &k \\
&3 &-2 &4 \\
&1 &1 &-2
\end{vmatrix} = (4-4)i + (4-(-6))j + (3-(-2))k = 10j + 5k
\]

所以法向量
\(n(0,10,5)\)

这就是通过向量积求得与两个向量都垂直的法向量的方法。


你甚至可以使用 C# 手撸这个算法出来:

var A = torch.tensor(new double[] { 3.0, -2, 4 });

var B = torch.tensor(new double[] { 1.0, 1.0, -2.0 });
var cross = Cross(A, B);
cross.print();

static Tensor Cross(Tensor A, Tensor B)
{
    if (A.size(0) != 3 || B.size(0) != 3)
    {
        throw new ArgumentException("Both input tensors must be 3-dimensional.");
    }

    var a1 = A[0];
    var a2 = A[1];
    var a3 = A[2];
    var b1 = B[0];
    var b2 = B[1];
    var b3 = B[2];

    var i = a2 * b3 - a3 * b2;
    var j = a3 * b1 - a1 * b3;
    var k = a1 * b2 - a2 * b1;

    return torch.tensor(new double[] { i.ToDouble(), -j.ToDouble(), k.ToDouble() });
}
[3], type = Float64, device = cpu 0 -10 5

由于当前笔者所用的 C# 版本的 cross 函数不对劲,不能直接使用,所以我们也可以利用内核函数直接扩展一个接口出来。

public static class MyTorch
{
    [DllImport("LibTorchSharp")]
    public static extern IntPtr THSLinalg_cross(IntPtr input, IntPtr other, long dim);

    public static Tensor cross(Tensor input, Tensor other, long dim = -1)
    {
        var res = THSLinalg_cross(input.Handle, other.Handle, dim);
        if (res == IntPtr.Zero) { torch.CheckForErrors(); }
        return torch.Tensor.UnsafeCreateTensor(res);
    }
}
var A = torch.tensor(new double[] { 3.0, -2, 4 });

var B = torch.tensor(new double[] { 1.0, 1.0, -2.0 });

var cross = MyTorch.cross(A, B);
cross.print_csharp();
[3], type = Float64, device = cpu, value = double [] {0, 10, 5}


当前笔者所用版本 other 参数是 Scalar 而不是 Tensor,这里应该是个 bug,最新 main 分支已经修复,但是还没有发布。

image-20241109024627974


另外,还有一种通过夹角求得法向量的方法,如果知道两个向量的夹角,也可以求向量积,公式如下:

\[a \times b = |a| \cdot |b| \sin\alpha
\]


一般来说,对于空间求解问题,我们往往是可以计算向量积的,然后通过向量积得出
\(|a| \cdot |b| \sin\alpha\)
的结果,而不是通过
\(|a| \cdot |b| \sin\alpha\)
求出
\(a \times b\)

关于此条公式,这里暂时不深入。


直线和平面表示法

在本小节节中,我们将学习空间中的直线和平面的一些知识。

在空间中的平面,可以使用一般式方程表达:

\[v = Ax + By + Cz + D
\]


其中 A、B、C 是法向量的坐标,即
\(n = \{A,B,C\}\)


首先,空间中的直线有三种表示方法,分别是对称式方程、参数式方程、截距式方程。


直线的对称式方程

给定空间中的一点
\(P_{0}(x_{0},y_{0},z_{0})\)
有一条直线 L 穿过
\(p_{0}\)
点,以及和非零向量
\(v=\{l,m,n\}\)
平行。

image-20241109150817967


直线上任意一点和
\(p_{0}\)
的向量都和
\(v\)
平行,
\(\overrightarrow{P_{0}P} =\{x - x_{0},y - y_{0}, z - z_{0}\}\)
,所以其因为其对应的坐标成比例,所以其截距式方程为:

\[\frac{x-x_{0}}{l} = \frac{y-y_{0}}{m} =\frac{z-z_{0}}{n}
\]


直线的参数式方程

因为:

\[\frac{x-x_{0}}{l} = \frac{y-y_{0}}{m} =\frac{z-z_{0}}{n} = t
\]


所以:

\[\begin{cases}x = x_{0} + lt
\\y = y_{0} + mt
\\z = z_{0} + nt

\end{cases}
\]


这便是直线的参数式方程。

直线的一般式方程

空间中的直线可以看作是两个平面之间的交线,所以直线由两个平面的一般式方程给出:

\[\begin{cases}v_{1} = A_{1}x + B_{1}y + C_{1}z + D_{1}
\\ v_{2} = A_{2}x + B_{2}y + C_{2}z + D_{2}

\end{cases}
\]


这些公式在计算以下场景问题时很有帮助,不过本文不再赘述。


① 空间中任意一点到平面的距离。

② 直线和平面之间的夹角。

③ 平面之间的夹角。


矩阵

矩阵在在线性代数中具有很重要的地位,深度学习大量使用了矩阵的知识,所以读者需要好好掌握。

如下图所示,A 是一个矩阵,具有多行多列,
\(a_{11}、a_{12}、...、a_{1n}\)
是一个行,
\(a_{11}、a_{21}、...、a_{m1}\)
是一个列。

image-20240910115046782


在 C# 中,矩阵属于二维数组,即
\(m*n\)
,例如要创建一个
\(3*3\)
的矩阵,可以这样表示:

var A = torch.tensor(new double[,]
{
    { 3.0, -2.0, 4.0 },
    { 3.0, -2.0, 4.0 },
    { 3.0, -2.0, 4.0 }
});

A.print_csharp();


使用
.T
将矩阵的行和列倒过来:

var A = torch.tensor(new double[,]
{
    { 3.0, -2.0, 4.0 }
});

A.T.print_csharp();


生成的是:

{
	{3.0},
	{-2.0},
	{4.0}
}

如图所示:

image-20241109154450656


矩阵的加减

矩阵的加减法很简单,就是相同位置的数组加减。

var A = torch.tensor(new double[,]
{
    { 1.0, 2.0, 4.0 },
    { 1.0, 2.0, 4.0 },
    { 1.0, 2.0, 4.0 }
});

var B = torch.tensor(new double[,]
{
    { 1.0, 1.0, 2.0 },
    { 1.0, 1.0, 2.0 },
    { 1.0, 1.0, 2.0 }
});

(A+B).print_csharp();

结果是:

{ 
    {2, 3, 6}, 
    {2, 3, 6}, 
    {2, 3, 6}
}


如果直接将两个矩阵使用 Pytorch 相乘,则是每个位置的数值相乘,这种乘法称为 Hadamard 乘积:

var A = torch.tensor(new double[,]
{
    { 1.0, 2.0 }
});

var B = torch.tensor(new double[,]
{
    { 3.0, 4.0 }
});

// 或者 torch.mul(A, B)
(A * B).print_csharp();
[2x1], type = Float64, device = cpu, value = double [,] { {3}, {8}}


矩阵乘法

我们知道,向量内积可以写成
\(x_{2}x_{1}+y_{2}y_{1}+z_{2}z_{1}\)
,如果使用矩阵,可以写成:

\[\begin{bmatrix}
&x_{1} &y_{1} &z_{1} \\
\end{bmatrix} \cdot
\begin{bmatrix}
&x_{2} \\
&y_{2} \\
&z_{2}
\end{bmatrix} = x_{2}x_{1}+y_{2}y_{1}+z_{2}z_{1}
\]


换成实际案例,则是:

\[\begin{bmatrix}
&1 &2 &3\\
\end{bmatrix} \cdot
\begin{bmatrix}
&4 \\
&5 \\
&6
\end{bmatrix} = 1*4 + 2*5 + 3*6 = 32
\]


使用 C# 计算结果:

var a = torch.tensor(new int[] { 1, 2, 3 });
var b = torch.tensor(new int[,] { { 4 }, { 5 }, { 6 } });

var c = torch.matmul(a,b);
c.print_csharp();
[1], type = Int32, device = cpu, value = int [] {32}


上面的矩阵乘法方式使用 **A ⊗ B ** 表示,对于两个多行多列的矩阵乘法,则比较复杂,下面单独使用一个小节讲解。


**A ⊗ B **

矩阵的乘法比较麻烦,在前面,我们看到一个只有行的矩阵和一个只有列的矩阵相乘,结果只有一个值,但是对于多行多列的两个矩阵相乘,矩阵每个位置等于 A 矩阵行和 B 矩阵列相乘之和。


比如下面是一个简单的
2*2
矩阵。

\[\begin{bmatrix}
&a_{11} &a_{12} \\
&a_{21} &a_{22}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{11} &b_{12} \\
&b_{21} &b_{22}
\end{bmatrix}
=

\begin{bmatrix}
&c_{11} &c_{12} \\
&c_{21} &c_{22}
\end{bmatrix}
\]


因为
\(c_{11}\)
是第一行第一列,所以
\(c_{11}\)
是 A 矩阵的第一行乘以 B 第一列的内积。

\[c_{11} =
\begin{bmatrix}
&a_{11} &a_{12}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{11} \\
&b_{21}
\end{bmatrix}
= a_{11}*b_{11}+a_{12}*b_{21}
\]


因为
\(c_{12}\)
是第一行第二列,所以
\(c_{12}\)
是 A 矩阵的第一行乘以 B 第二列的内积。

\[c_{12} =
\begin{bmatrix}
&a_{11} &a_{12}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{12} \\
&b_{22}
\end{bmatrix}
= a_{11}*b_{12}+a_{12}*b_{22}
\]


因为
\(c_{21}\)
是第二行第一列,所以
\(c_{21}\)
是 A 矩阵的第二行乘以 B 第一列的内积。

\[c_{21} =
\begin{bmatrix}
&a_{21} &a_{22}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{22} \\
&b_{21}
\end{bmatrix}
= a_{21}*b_{11}+a_{22}*b_{21}
\]


因为
\(c_{22}\)
是第二行第二列,所以
\(c_{22}\)
是 A 矩阵的第二行乘以 B 第二列的内积。

\[c_{22} =
\begin{bmatrix}
&a_{21} &a_{22}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{12} \\
&b_{22}
\end{bmatrix}
= a_{21}*b_{12}+a_{22}*b_{22}
\]


例题如下:

\[\begin{bmatrix}
&1 &2 \\
&3 &4
\end{bmatrix} \cdot
\begin{bmatrix}
&5 &6 \\
&7 &8
\end{bmatrix}

=

\begin{bmatrix}
&(1*5 + 2*7) &(1*6 + 2*8) \\
&(3*5 + 4*7) &(3*6 + 4*8)
\end{bmatrix}
=
\begin{bmatrix}
&19 &22 \\
&43 &50
\end{bmatrix}
\]


使用 C# 计算多行多列的矩阵:

var A = torch.tensor(new double[,]
{
    { 1.0, 2.0 },
    { 3.0, 4.0 }
});

var B = torch.tensor(new double[,]
{
     { 5.0 , 6.0 },
     { 7.0 , 8.0 }
});

torch.matmul(A, B).print_csharp();
{ {19, 22}, {43, 50}}

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: AlignSum: Data Pyramid Hierarchical Fine-tuning for Aligning with Human Summarization Preference

创新点


  • 发现在文本摘要任务中,预训练语言模型在自动评估与人工评估中表现不一致,原因可能是低质量的训练数据。
  • 考虑到标注成本,论文提出了一种新的人类摘要偏好对齐框架
    \({\tt AlignSum}\)
    ,使用提取、
    LLM
    生成和人工标注等多种方法构建数据金字塔,能够充分利用极其有限的高质量数据来提升预训练语言模型(
    PLMs
    )在摘要生成方面的能力极限。

内容概述


文本摘要任务通常使用预训练语言模型(
PLMs
)来适应各种标准数据集。尽管这些
PLMs
在自动评估中表现出色,但在人工评估中常常表现不佳,这表明它们生成的摘要与人类摘要偏好之间存在偏差。这种差异可能是由于低质量的微调数据集,或者是能反映真正的人类偏好的高质量人类标注数据有限。

注释大量高质量摘要数据集是不切实际的,论文希望不再依赖于对大量训练数据进行传统的简单微调,而是充分利用极其有限的高质量数据来提升预训练语言模型(
PLMs
)在摘要生成方面的能力极限。

为了解决这个挑战,论文提出了一种新的人类摘要偏好对齐框架
\({\tt AlignSum}\)
。该框架由三个部分组成:首先,构建一个数据金字塔,其中包含抽取式、生成式和人类标注的摘要数据。其次,进行高斯重采样,以去除极端长度的摘要。最后,在高斯重采样后实现两阶段的分层微调与数据金字塔的结合。


\({\tt AlignSum}\)
应用到人类标注的
CNN
/
DailyMail

BBC XSum
数据集中,像
BART-Large
这样的
PLMs
在自动评估和人工评估中都超越了
175B

GPT-3
。这证明了
\({\tt AlignSum}\)
显著增强了语言模型与人类摘要偏好的对齐。

AlignSum


整体框架包含三个部分:

  1. 使用提取、
    LLM
    生成和人工标注等多种方法构建数据金字塔(
    Data Pyramid
    )。
  2. 由于源数据具有不同的摘要长度,利用高斯重新采样来调整生成摘要的长度,以接近目标长度。
  3. 采用了两阶段的层次微调策略:初始阶段对
    PLMs
    进行抽取式和生成式数据的训练,以适应一般领域,然后在人工标注数据上对刚刚微调过的
    PLMs
    进行进一步微调,以使其与人类偏好对齐。

构建数据金字塔

数据金字塔由三个层级组成,从下到上按质量和获取难度递增,而数量则递减。前两者是摘要生成领域中最常见的两种风格,将它们统称为通用数据。最后一层是用于对齐人类偏好的最关键部分,称之为个性化数据。

  • 抽取式数据

抽取式数据构成了预训练语料库的主要部分,并且是最容易获得的。参考
GSG
,使用
ROUGE-1
指标来计算相似性,并遍历整个文档以找到与之最相似的句子作为伪摘要
\(\hat{S}\)

\[\begin{equation}
\begin{split}
&\ \ r_i = \mathrm{Rouge} (d_i, D_{\setminus d_i}), \\
&\ \ \hat{S} = \mathrm{argmax}_{d_i} \{r_i\}_{i=1}^n.
\end{split}
\end{equation}
\]

  • 生成式数据

抽取式数据有助于识别文档中的重要句子,但不足以总结跨越多个句子的关键信息。相比之下,
LLMs
(大规模语言模型)是有效的零样本摘要生成器,能够提取跨句子及文档级别的摘要信息。

使用系统提示和用户提示引导
LLMs
对文档
\(D\)
进行摘要,并生成伪摘要
\(\hat{S}\)
。系统提示指定了准确摘要生成的一般要求,然后在用户提示之前插入文档,确保
LLM
能够阅读整个文档并遵循用户要求。用户提示是数据集特定的,设定所需的摘要长度和单词数量。

  • 人类标注数据

通过使用上述两种数据进行训练,
PLMs
(预训练语言模型)获得了领域特定的知识。为了生成符合人类偏好的摘要,进一步在人类标注数据上进行微调是必要的。

为了避免随机注释的差异性,使用
Element-aware
数据集。该数据集遵循特定指令,结合了微观和宏观需求,确保一致且高质量的人类注释。

高斯重采样

三个不同的数据源的伪摘要都有独特的标记长度分布,其中抽取式和抽象数据的摘要标记长度分布存在明显差异。因此,直接使用这些不同的分布进行训练可能会导致生成过长或过短的摘要。

为了解决这个问题,引入了高斯重采样技术,以使所有摘要长度与人类注释的摘要对齐。

将人类标注数据的标记长度分布建模为高斯分布。在
95%
概率的 [
\(\mu - 2\sigma\)
,
\(\mu + 2\sigma\)
]区间内对抽取式和抽象数据进行重采样,以去除具有过长或过短伪摘要的样本。

两阶段层级微调

直接对预训练语言模型(
PLMs
)进行微调可能会很具挑战性,因为少量的高熵数据对于对齐至关重要,但可能会受到大量低熵数据的信息干扰,从而导致数据金字塔的未充分利用。

为了避免这个潜在问题,论文提出两阶段的分层微调策略。给定一个预训练语言模型
\(p_{\theta}\)

  1. 首先通用微调阶段,使用抽取式和抽象数据对
    \(p_{\theta}\)
    进行微调,以增强其生成领域通用摘要的能力,从而获得模型
    \(p_{\theta'}\)
  2. 接下来是个性化微调阶段,使用人类标注数据对
    \(p_{\theta'}\)
    进行微调,以创建与人类偏好对齐的最终模型
    \(p_{\theta''}\)

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

在微信群里看到有同学对.NET 9的贡献者数量有质疑,.NET 这样的一个全场景的应用开发平台,他的生态是很庞大的,自然一起参与开源贡献的开发者也是很大的,但是很多人都不知道一直有这么一个地址是统计了.NET各个版本的开发者数量的,这篇文章就是给大家统计显示一下各个版本的.NET贡献者人数.

.NET  Core 1.0 一共有12870 贡献:
https://dotnet.microsoft.com/en-us/thanks/1.0

image

.NET Core 2.0 一共有618 贡献:
https://dotnet.microsoft.com/en-us/thanks/2.0

image

.NET Core 3.0 一共有34108贡献:
https://dotnet.microsoft.com/en-us/thanks/3.0

image

.NET Core 3.1一共有9491 贡献:
https://dotnet.microsoft.com/en-us/thanks/3.1

image

.NET 5.0一共有49900 贡献:
https://dotnet.microsoft.com/en-us/thanks/5.0

image

.NET 6.0一共有243366 贡献:
https://dotnet.microsoft.com/en-us/thanks/6.0

image

.NET 7.0 一共有155976 贡献:
https://dotnet.microsoft.com/en-us/thanks/7.0

image

.NET 8.0 一共有78000 贡献:
https://dotnet.microsoft.com/en-us/thanks/8.0

image

.NET 9一共有49946 贡献:
https://dotnet.microsoft.com/en-us/thanks/9.0

image