技术细节

  • MySQL/InnoDB 的 隐式锁

    ·

    InnoDB 的锁容易被忽略的细节是关于“隐式锁”(即:implicit locks)的存在。表现上,有的锁是存在的,但在使用SHOW ENGINE INNODB STATUS或者performance_schema.data_locks中却查看不到。最为常见的隐式锁是在写入(INSERT)时,当前事务会持有该记录对应的锁,但是在系统中,通常是查看不到的。但,如果发生了该锁冲突(或竞争)时,系统中则可以看到此类锁信息。

    本文重现了较为常见的隐式锁场景,包括:数据写入(INSERT)时的隐式锁、根据主键操作是可能产生的二级索引隐式锁等。帮助开发者能够更系统的理解,InnoDB 的锁机制。

    写入数据产生的隐式锁

    准备数据

    DROP TABLE IF exists t1;
    
     CREATE TABLE `t1` (
      `id` int unsigned,
      `nick` varchar(32),
      `age` int,
      UNIQUE KEY `uk_n` (`nick`)
    );
    mysql> desc t1;
    +-------+--------------+------+-----+---------+
    | Field | Type         | Null | Key | Default |
    +-------+--------------+------+-----+---------+
    | id    | int unsigned | YES  |     | NULL    |
    | nick  | varchar(32)  | YES  | UNI | NULL    |
    | age   | int          | YES  |     | NULL    |
    +-------+--------------+------+-----+---------+

    mysql> INSERT INTO t1 VALUES (1, 'a' , 12),(20,'z',29);
    Query OK, 2 rows affected (0.01 sec)
    Records: 2  Duplicates: 0  Warnings: 0
    
    mysql> select * from t1;
    +------+------+------+
    | id   | nick | age  |
    +------+------+------+
    |    1 | a    |   12 |
    |   20 | z    |   29 |
    +------+------+------+
    2 rows in set (0.00 sec)

    构建隐式锁

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO t1 VALUES (8, 'h' , 32);
    Query OK, 1 row affected (0.00 sec)

    查看锁信息:

    > SELECT 
        ENGINE_TRANSACTION_ID AS TRX_ID,
        OBJECT_NAME,
        INDEX_NAME,
        LOCK_TYPE,
        LOCK_MODE,
        LOCK_STATUS,
        LOCK_DATA 
      FROM performance_schema.data_locks;
    +--------+-------------+------------+-----------+-----------+-------------+-----------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
    +--------+-------------+------------+-----------+-----------+-------------+-----------+
    |  10165 | t1          | NULL       | TABLE     | IX        | GRANTED     | NULL      |
    +--------+-------------+------------+-----------+-----------+-------------+-----------+

    可以看到,在 data_locks 表中没有任何关于事务中写入数据相关的锁。这是,因为这是一个隐式的锁,在没有任何锁竞争的情况下,系统并不会将该类型的锁展示出来(注:这可能与底层的存储和实现有关,隐式锁在实现上可能就没有“显式”的存储在锁相关的数据结构中)。

    构建锁竞争/隐式转显式

    这里通过在另一个事务中尝试并发写入一条冲突的记录,来构建锁竞争:

    mysql> START TRANSACTION;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO  t1 VALUES (9,'h',17);
    ...

    该事务执行时,则会陷入锁等待。这时,再次查看锁信息如下:

    +--------+-------------+------------+-----------+---------------+-------------+---------------------+
    | TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA           |
    +--------+-------------+------------+-----------+---------------+-------------+---------------------+
    |  10165 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL                |
    |  10168 | t1          | NULL       | TABLE     | IX            | GRANTED     | NULL                |
    |  10165 | t1          | uk_n       | RECORD    | X,REC_NOT_GAP | GRANTED     | 'h', 0x000000000214 |
    |  10168 | t1          | uk_n       | RECORD    | S             | WAITING     | 'h', 0x000000000214 |
    +--------+-------------+------------+-----------+---------------+-------------+---------------------

    这时候,可以看到事务10165,持有一个记录锁,该锁是一个排它记录锁(X,REC_NOT_GAP ),加锁对象是'h', 0x000000000214(注,这是一个唯一索引的入口,前面'h'是唯一索引值,后面的0x000000000214部分是该表的InnoDB内置rowid)。

    主键/二级索引操作相关的隐式锁

    InnoDB 的锁管理和实现确实一个超级复杂的部分(”mega-complicated“)。隐式锁的使用场景也非常多,如果对此不了解的话,那么在观察 InnoDB 的锁信息时,是会有很多的困惑的。这里再列举一类也算,较为常用的隐式锁:“主键索引/二级索引”相关的隐式说。即:

    • 当对记录进行操作时,即便是通过主键扫描,也可能对二级索引进行加锁
    • 当对记录进行操作时,即便是通过二级索引扫描,也可能对主键进行加锁

    这类场景的加锁,通常都是会存在隐式锁。

    主键操作时二级索引上的隐式锁

    在下面的测试中,我们先主键 id = 8的记录进行删除操作,然后通过系统表data_locks观察该事务是否持有二级索引相关的锁;而后,在另一个事务中,通过二级索引(nick = 'Henry')对该记录进行操作(共享读),而后再重新观察前面事务的锁状态。

    在下面的测试可以观察到,在Session B没有开始前;在 Session ADELETE语句是观测不到二级索引上的锁的;但当Session B尝试去锁定二级索引上的入口时,再次观察Session A上的锁信息,就可以看到,在Session A没有任何操作的情况下,多出了一个额外的、持有的二级索引上的锁,该锁原本是一个“隐式锁”,在发生锁竞争后,转化为一个“显式锁”。即便是在Session B因为等待操作或结束了,Session A持有的已经转化的“显式锁”也不会再回退了。详细测试如下。

    准备数据

    DROP TABLE IF exists t1;
     CREATE TABLE `t1` (
      `id` int unsigned,
      `nick` varchar(32),
      `age` int,
      PRIMARY KEY (`id`),
      UNIQUE KEY `uk_n` (`nick`)
    );
    mysql> INSERT INTO t1 VALUES (1,"Alice",12);
    mysql> INSERT INTO t1 VALUES (8,"Henry",27);
    mysql> INSERT INTO t1 VALUES (16,"Peter",15);
    mysql> show variables like '%iso%';
    +-----------------------+----------------+
    | Variable_name         | Value          |
    +-----------------------+----------------+
    | transaction_isolation | READ-COMMITTED |
    +-----------------------+----------------+

    构建隐式锁

    Session A

    mysql> START TRANSACTION;
    mysql> DELETE FROM t1 WHERE id = 8;
    
    mysql> SELECT
        ->     ENGINE_TRANSACTION_ID AS TRX_ID,
        ->     INDEX_NAME,LOCK_TYPE,
        ->     LOCK_MODE, LOCK_STATUS,
        ->     LOCK_DATA
        ->   FROM performance_schema.data_locks;
    +--------+------------+-----------+---------------+-------------+-----------+
    | TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
    +--------+------------+-----------+---------------+-------------+-----------+
    |  10317 | NULL       | TABLE     | IX            | GRANTED     | NULL      |
    |  10317 | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 8         |
    +--------+------------+-----------+---------------+-------------+-----------+

    Session B

    隐式锁转换为显式锁

    继续上述两个Sessions的操作:

    Session A

    Session B

    mysql> START TRANSACTION;
    mysql> SELECT * FROM t1 WHERE nick = 'Henry' FOR SHARE;
    ( Waiting )
    ...(Query Locks From performance_schema.data_locks like above)...
    +-----------------+------------+-----------+---------------+-------------+------------+
    | TRX_ID          | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA  |
    +-----------------+------------+-----------+---------------+-------------+------------+
    |           10317 | NULL       | TABLE     | IX            | GRANTED     | NULL       |
    | 421929568337920 | NULL       | TABLE     | IS            | GRANTED     | NULL       |
    |           10317 | uk_n       | RECORD    | X,REC_NOT_GAP | GRANTED     | 'Henry', 8 |
    | 421929568337920 | uk_n       | RECORD    | S,REC_NOT_GAP | WAITING     | 'Henry', 8 |
    |           10317 | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 8          |
    +-----------------+------------+-----------+---------------+-------------+------------+
    (... Abort last statement...)
    ERROR 1205 (HY000): Lock wait timeout exceeded; 
    try restarting transaction
    ...(Query Locks From performance_schema.data_locks like above)...
    +--------+------------+-----------+---------------+-------------+------------+
    | TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA  |
    +--------+------------+-----------+---------------+-------------+------------+
    |  10317 | NULL       | TABLE     | IX            | GRANTED     | NULL       |
    |  10321 | NULL       | TABLE     | IS            | GRANTED     | NULL       |
    |  10317 | uk_n       | RECORD    | X,REC_NOT_GAP | GRANTED     | 'Henry', 8 |
    |  10317 | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 8          |
    +--------+------------+-----------+---------------+-------------+------------+

    最后

    一些理解

    “隐式锁”可以理解为,在某些条件下,这里一定是存在“锁”的,所以,既然一定是存在的,并且这类场景可能还比较广泛,那么为了节省存储空间与操作,就省略了此类“锁”的表示。例如,通常,如果事务写入了一条数据,那么该事务一定是持有该数据的排它锁的。

    但,当真的有其他事务也尝试去获取该“隐式锁”的时候,那么为了便于进行锁检测与管理,则会重新将该锁表示出来。并且,也不再有必要重新转化为隐式锁。

    所以,如果有人问,你是否可以把当前数据库的所有的锁情况,都打印或记录下来,这是做不到的,也是没有必要的。事实上,“隐式锁”是广泛存在的,但因为通常并没有那么锁竞争,这些“隐式锁”也就一直不会被表示出来。

    一块拼图

    通常,隐式锁是可以被忽略的,如上述示例,这可能是一个没有任何竞争的锁。但,当出现对应的锁竞争时,则会变得可见。一般地,是可以不用关注隐式锁的,但,如果希望能够对 InnoDB 锁有非常系统的了解,这也是一块重要的“拼图”。

  • 存储引擎在存储整数时,一般会使用最高位作为标志位,标记存储的整数是正数还是负数(参考),最高位也被称为“most significant bit (MSb)”。通常,最高位为1则表示正数,最高位为0,则表示负数。更进一步的,负数则会通过补码(参考:two’s complement)的方式表示。但是,InnoDB没有使用这种方法。

    InnoDB 的整数存储

    在死锁诊断时,偶然注意到,InnoDB 在存储整数时,与一般的系统是不同的。例如,int 类型存储 1 的时候,使用的表示是:0x80000001。更多的示例可以参考右图:

    整数值InnoDB 表示
    10x80000001
    -10x7fffffff
    70x80000007
    -70x7ffffff9

    可以看到,这与一般的有符号型的整数存储是相反的。即:

    • 正数表示时,最高位(MSb)为1
    • 负数表示时,最高位(MSb)为0

    关于这个问题,在 Stackoverflow上也有看到有部分用户有类似的疑问:

    本文将讨论为什么会这样。

    考虑 8-bit 场景下的

    这里来回顾一下“体系结构”中的最为基础的一些知识吧。

    整数值绝对值绝对值的二进制原码2-补码Offset binary(“移码”)
    110000-00010000-00010000-00011000-0001
    -110000-00011000-00011111-11110111-1111
    770000-01110000-01110000-01111000-0111
    -770000-01111000-01111111-100101111001

    说明:

    移码有两种计算方式,结果是等价的,即:

    • 直接将原始数据加上2(n-1),然后转化为二进制即可以
    • 将其补码,最高位进行一次翻转,即 “补码 XOR 2(n-1)

    验证存储方式

    为了确认 InnoDB 的整数处理,再MySQL 8.4.4的源码中找到如下 InnoDB 处理整型数据的代码:

      if (type == DATA_INT) {
        /* Store integer data in Innobase in a big-endian format,
        sign bit negated if the data is a signed integer. In MySQL,
        integers are stored in a little-endian format. */
    
        byte *p = buf + col_len;
    
        for (;;) {
          p--;
          *p = *mysql_data;
          if (p == buf) {
            break;
          }
          mysql_data++;
        }
    
        if (!(dtype->prtype & DATA_UNSIGNED)) {
          *buf ^= 128;
        }
    
        ptr = buf;
        buf += col_len;

    这段代码中,先将字节序做了颠倒(从最高字节位开始,逐个字节进行拷贝存储),即将 MySQL 层面的小端(little-endian)转化为了InnoDB层面的(big-endian)存储。而后,再对最高位进行了一次翻转,即这里的:*buf ^= 128操作。

    即:先将数据在MySQL层面的表示做了大小端的转化并拷贝过来,然后,将最高位进行翻转。即,先将2补码的表示模式拷贝过来,再将最高位进行翻转。

    什么要这么存储

    在 MySQL/InnoDB 官方文档或者代码中,并没有关于该实现的说明。不过这么做,有一个非常明显的好处,即所有的整数表示的大小关系,与实际存储的数据(当中无符号型对待)的大小关系是一致的。

    即,在上述的例子中:7 > 1 > -1 > -7,而对应的编码表示,也有对应的大小关系:

    0x80000007 > 0x80000001 >0x7fffffff > 0x7ffffff9

    这里对这个问题做一个简单探讨。先说结论吧,这是一种较为典型的整数编码方式:Offset binary(“移码”)。即,将需要表示的整数,加上一个数值,让所有的整数映射到自然数空间。例如,在MySQL中使用32位的int类型,需要表示的整数范围为[-231,231]。那么,实际表示时,则加上231。更为一般的,对于[-2(n-1), 2(n-1)]之间的所有整数在表示时,都加上了2(n-1)。即,建立的映射关系是:

    f(i) = i + 2(n-1)

    即对于任何要存储的整数i,实际存储时都存储上述的f(i)。而在实际运算时,则是,将补码的最高位进行一次翻转即可。

    关于补码

    例如,在 8 位二进制中,00000001 表示 +1,而 11111111 代表 -1。具体的,在表示-3 时,先取 3 的二进制 00000011,再逐位取反 11111100,最后加 1 得到 11111101,即 -3 的补码表示。这种方式让计算机能够高效地进行整数运算,是典型的正负数的方法,该方法的更多优势可以参考:two’s complement

    补充说明

    MySQL 层面的整数表示和 InnoDB 的整数存储是不同的。在“验证存储方式”小结中的代码中可以看到:

    • MySQL使用了小端(little-endian),InnoDB层面使用了大端(big-endian)存储
    • 在 MySQL 层面使用2-补码做有符号整数类型存储;而InnoDB层面使用了“移码”存储

    参考文档

  • 理解 MySQL 隐式主键

    ·

    隐式主键是 MySQL 8.0 版本新增的一个重要特性。可以非常好的解决了诸如无主键大表更新时的主备延迟问题,大大提升了主备高可用架构的“可用性”。

    为什么需要隐式主键

    最早不得不引入隐式主键功能的,大概是云厂商。

    很早,在 MySQL 运维的过程中就发现了有一类复制延迟问题,非常难缠。当主库的表没有主键/唯一键时,在主库使用一条 UPDATEDELETE操作了大量记录,在使用ROW模式的备库中,则会收到对应的、大量的变更记录,而这些变更记录在备库上应用(apply)时,因为没有主键或者唯一索引,每一条变更的回放都需要很长时间,最终导致主备之间无法追上的延迟。

    所以,在很早的时候,MySQL 规范中就有一条,表必须要有主键。对于企业,也许可以通过规范,或者调整表结构去绕开这个问题,但是,对于提供数据库托管服务的云厂商来说,却没法去要求上面使用数据库的用户去做任何适配。但是,云厂商有需要为这些数据库服务提供基于主备的高可用能力。这就陷入了一个困境,这也是为什么云厂商可能是最早需要解决这个问题的。

    早在 2016 年,阿里云的 RDS 就已经通过引入隐式主键解决类似的问题:MySQL · 最佳实践 · RDS 只读实例延迟分析

    MySQL的实现方案

    相比于社区的实现,MySQL 官方的实现考虑的更加全面,首先引入不可见列、不可见索引等特性,然后再在此基础上实现隐式主键,也全面的考虑对历史版本的兼容性、对复制的影响、对备份的影响、对各类操作命令的影响等。

    在 MySQL 8.0.30 版本(2022年07月)中,官方MySQL正式引入了隐式主键的功能。对于所有没有显式主键的 InnoDB 表,都会新增一个如下的隐式主键:

    my_row_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT INVISIBLE PRIMARY KEY

    所以,甚至在你新建一个 InnoDB 表时,如果你没有显式的主键,那么字段名 my_row_id就不能再使用了。也因为该版本是通过 INVISIBLE COLUMN 实现的,所以可以通过ALTER TABLE t1 CHANGE COLUMN...命令将隐式主键转换为普通列。

    打开隐式主键功能

    • 参数 show_gipk_in_create_table_and_information_schema 则可以控制在SHOW以及 information_schema中是否展示隐式主键信息,该参数可以帮助使用SHOW以及 information_schema的应用程序,依旧保持很好的兼容性。

    其他相关的参数包括:

    • sql_require_primary_key :该参数可以强制要求数据库中的表尽量有主键。例如,创建表、ALTER表时都需要表有主键;删除表的主键失败等,总之,尽可能的要求表均有主键。
    • REQUIRE_TABLE_PRIMARY_KEY_CHECK 这是复制配置时的选项,该参数控制的是复制时的应用线程(apply)如何检查表是否有主键,该选项的取值为:{STREAM | ON | OFF | GENERATE}。该参数可以很好的控制,从主库复制过来的表,对主键配置的要求。

    DDL 、复制与Binlog

    如果MySQL开启了隐式主键,那么就像invisible column一样,CREATE TABLEALTER TABLE的创建的隐式主键也会存储在 Binlog 中,所以备库如果在复制时,也可以活动对应的信息。

    mysql> set session sql_generate_invisible_primary_key=ON;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> create table t1_no_pk(n char(10),age int);
    Query OK, 0 rows affected (0.02 sec)
    
    mysql> show create table t1_no_pk\G
    *************************** 1. row ***************************
           Table: t1_no_pk
    Create Table: CREATE TABLE `t1_no_pk` (
      `my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT /*!80023 INVISIBLE */,
      `n` char(10) DEFAULT NULL,
      `age` int DEFAULT NULL,
      PRIMARY KEY (`my_row_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
    1 row in set (0.00 sec)

    再来使用mysqlbinlog命令看看对应 binlog格式:

    # at 653
    #250316 15:02:05 server id 1  end_log_pos 928 CRC32 0x9ca72462 	Query	thread_id=9	exec_time=0	error_code=0	Xid = 31
    SET TIMESTAMP=1742108525/*!*/;
    /*!80013 SET @@session.sql_require_primary_key=0*//*!*/;
    CREATE TABLE `t1_no_pk` (
      `my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT /*!80023 INVISIBLE */,
      `n` char(10) DEFAULT NULL,
      `age` int DEFAULT NULL,
      PRIMARY KEY (`my_row_id`)
    )
    /*!*/;
    SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
    DELIMITER ;
    # End of log file
    /*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
    /*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

    参考阅读

  • 在2015年,由 OpenAI 的 DP Kingma 等发布了 《ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION》算法后,由于其迭代效率提升非常明显,所以 ADAM(或其变种)就被广泛的采用。本文将继续对上一篇介绍的梯度下降算法进行优化,并介绍 ADAM 算法(一种对随机梯度下降算法的优化算法)的实现以及效果。

    Stochastic Gradient Descent 或者说 mini-batch解决了样本量巨大时,梯度下降迭代的问题。但是,也带了一些新的问题。最为主要的是,因为样本数据的波动,而导致每次梯度下降计算时,梯度方向的波动,从而降低了梯度下降迭代的效率。

    在前面的《Mini-batch Gradient Descent和随机梯度下降(SGD)》文章中,我们对比了 mini-batch 和 batch gradient descent 的在迭代时,目标函数下降的速度。

    可以看到,batch gradient descent 的目标函数下降非常稳定,而 Mini-batch 的实现则会有明显的波动。为了尝试修正这个问题,从而提高迭代效率,在神经网络算法上,逐渐探索出了一些较为高效的优化算法:Adam SGD。该算法将 RMSprop 和 “Exponential smoothing”的想法结合在一起,形成了一个较为高效的算法,在实践中被广为使用。


    Stochastic Gradient Descent 与 Momentum

    SGD 会在每次迭代时根据样本的偏差,展现出不同的偏差,所以,在使用SGD进行迭代时,观察其 cost函数下降,应该会有更加明显的波动(后续吧自己实现的程序改造后,尝试观察一下)。

    为了加快迭代的速度,一个折中的思路是,引入一个均值替换当前的梯度方向。该如何引入这个均值呢?梯度是一个随时计算推进,不断推进的变量,常用的均值计算可以参考:Moving average。最为常见的实现是使用“Exponential moving average”,这种平均值的计算,在迭代计算时实现非常简单。

    Momentum 就是 “Exponential moving average”实现时的参数“smoothing factor”,在神经网络中,经常使用 \( \beta \)表示(原因是 \( \alpha \) 已经表示学习率了 )。

    而这里的 Momentum ,也是 TensorFlow 在构造 SGD 算法时需要的另一个参数。

    关于Exponential moving average

    或者叫“Exponential smoothing”。我们看看这个算法的具体实现是怎样的?

    原始的迭代:\( w = w – \alpha \frac{\partial J}{\partial w} \)

    使用 “Exponential smoothing” 后的迭代:

    $$
    \begin{align}
    v_0 & = 0 \quad \partial{w}_t = \frac{\partial J}{\partial w}|_{(for \, sample \, t)} \\
    v_{t} & = \beta*v_{t-1} + (1-\beta)\partial{w}_{t} \\
    w & := w – \alpha v_t
    \end{align}
    $$

    考虑 \( \beta = 0.9 \),如果数学直觉比较好的话,可以看出,原本使用梯度\( \partial{w} \)进行迭代的,这里使用了一个梯度的“Exponential smoothing” \( v_t \)去替代。上面的式子中,\( v_t \) 如果展开有如下表达式:

    $$
    \begin{align}
    v_t & = (1-\beta)\partial{w}_{t} + \beta(1-\beta)\partial{w}_{t-1} + \beta^2(1-\beta)\partial{w}_{t-2} … \\
    & = \sum\limits_{i=0}^{t} \beta^{i}(1-\beta)\partial{w}_{i}
    \end{align}
    $$

    使用“Exponential smoothing” 之后,新的迭代方向 \( v_t \),可以理解为一个前面所有梯度方向的加权平均。离得越近的梯度,权重越高,例如,\( \partial{w}_{t} \)的权重是\( (1-\beta) \);而之前的梯度,则每次乘以一个 \( \beta \)衰减。

    Exponential moving average的“冷启动问题”与修正

    仔细观测上诉的 “Exponential moving average” 公式,可以注意到一个问题,就是其最初的几个点总是会偏小。其原因是,当前值的权重总是为 \( 1- \beta \),而因为是初始的几个值,并没有更前面的数据去“平均”当前值,也就会出现,初始值总是会偏小的问题。

    通常,如果样本量很大的事时候,则可以忽略这个问题,因为初始值偏小的点占比会非常少,可以忽略。如果要一定程度上解决这个问题,也有继续对上述的 “Exponential moving average”做了一些修正,可以考虑对 \( v_t \)的结果值做一个修正:\( v_t := \frac{vt}{1-\beta^t} \)。

    一般的,因为样本的数量总是比较大的,所以我们可以忽略这个问题,而无需做任何修正。

    RMSprop

    在前面的“Gradient Descent with Momentum”中,我们看到为了解决梯度波动较大的问题,使用了 “Exponential moving average” 去尝试将一些比较偏的梯度,拉倒一个较为平均的方向上来。RMSprop的想法也是类似的,这里通过了root mean square的想法进行平均值的计算。具体的,在进行 SGD 时,每次更新梯度,按照如下的方法进行更新:

    $$
    \begin{align}
    s_0 & = 0 \quad \partial{w}_t = \frac{\partial J}{\partial w}|_{(for \, sample \, t)} \\
    s_{t} & = \beta*s_{t-1} + (1-\beta)(\partial{w}_{t})^2 \\
    w & := w – \alpha \frac{\partial w}{\sqrt{s_{t}}}
    \end{align}
    $$

    说明:这里对梯度进行平方时,如果在程序中是一个梯度向量,那么这里“平方”也就是对梯度的每一个分量进行一次平方。

    在“Exponential smoothing”的实现中,是将当前值,使用一个加权平均替代。与“Exponential smoothing”类似的,原本的梯度方向,现在使用如下的方向去替代了:

    $$
    \begin{align}
    s_t & = \frac{\partial{w}_{t}}{\sqrt{(1-\beta)(\partial{w}_{t})^2 + \beta(1-\beta)(\partial{w}_{t-1})^2 + \beta^2(1-\beta)(\partial{w}_{t-2})^2 + \cdots }} \\
    & = \frac{\partial{w}_{t}}{\sqrt{\sum\limits_{i=1}^{t}\beta^i(1-\beta)(\partial{w}_{i})^2}} \\
    \end{align}
    $$

    Adam Gradient Descent

    这可能是实际使用最多的算法,全称是 Adaptive Moment Estimation 。该实现,将 “Momentum” 和 “RMSprop” 做了一定的融合,形成了新的“最佳实践” Adam。在融合上,具体的实现与两个细节点:

    (1) 在 Adam 中均使用了“修正”计算,即 \( \hat{v_t} = \frac{v_t}{1-(\beta_1)^t} \quad \hat{s_t} = \frac{s_t}{1-(\beta_1)^t} \)

    (2) 参数更新公式,使用了两个算法的融合: \( w := w – \alpha \frac{\hat{v_t}}{\sqrt{\hat{s_t}}} \)

    Adam optimization的效果对比

    在 Adam 的论文中对于效果做了非常多的评估,感兴趣的可以参考相关论文。

    这里根据之前完成的训练程序,也进行了优化,实现了Adam算法。在 MNIST 数据集的训练上,我们来看看 Adam 的效果:

    从右图可以看到,Adam(蓝色)明显的提升了迭代效率。依旧一定程度存在 mini-batch(绿色) 的梯度波动的问题。相比于,batch gradient descent (红色)算法,迭代效率大大增加,约在第10次迭代,即在第一个epoch 的第十批样本进行训练时,cost 就下降到了比较低的程度。

    关于 root mean square

    root mean square也叫二次平均值,考虑一组数据:\( {x_1,x_2, \cdots , x_n } \),其RMS则为:

    $$ x_{rms} = \sqrt{\frac{1}{n} \sum_{i=1}^n x_i^2} = \sqrt{\frac{1}{n} (x_1^2 + x_2^2 + \cdots + x_n^2)} $$

    补充说明

    可以看到,所有的这些优化都是面向“最优化”问题的。梯度下降是一个一阶优化(First-order Optimization)的方法,其核心就在与每次迭代时,应该如何去更新响应的参数值,在梯度下降中也就是如何去选择合适的学习率。

    牛顿法是典型的二阶优化(Second-order Optimization),在迭代时使用了二阶导数,所以,通常可以获得更好的迭代效率。但是因为二阶导数的计算复杂度会上升非常多(对应的矩阵可能是所有参数的平方,应该也有人尝试去算过了…)。这也是为什么在这个场景下,依旧是使用一阶优化方法的原因。

    如果想比较好的理解学习率、Momentum、RMSprop、Adam等内容,建议先了解梯度、数值方法、最优化问题等数学方法。

    到这里这个系列算是一个小阶段了,这是一个个人学习的笔记,从数学的梯度概念开始,逐步到神经网络训练的Adam优化算法,也包含部分动手实践的神经网络算法实现。完成的系列包括了:

  • Terraform 可以自动化的创建云端的资源,但是要想实现更高的灵活度,则需要更为灵活的使用Terraform的“Data Sources”能力。例如,在自动化的创建数据库时,云厂商允许创建的版本号是在动态变化的,例如,当前最新的允许的创建的MySQL版本通常是 8.0.40,但通常过了一个季度之后,就变成了 8.0.41。这时,对应的 Terraform 的脚本就需要调整或者传递参数就需要发生变化。而 Terraform 提供的 “Data Sources” 能力则可以很好的解决这个问题。

    在 Oracle 的 Terraform 中可以使用 “Data Source: oci_mysql_mysql_versions” 实现该能力。

    示例

    首先使用 data 命令定义该对象:

    data "oci_mysql_mysql_versions" "gmv" {
        compartment_id = oci_identity_compartment.oic.id
    }

    这里会获取该租户环境下支持的所有MySQL版本。

    然后,再使用 output 命令就可以获取并输出这些版本信息。详细的output命令如下:

    output "mysql_version" {
      value       = data.oci_mysql_mysql_versions.gmv.versions
    }

    详细的输出示例如下:

    mysql_version = tolist([
      {
        "version_family" = "8.0"
        "versions" = tolist([
          {
            "description" = "8.0.36"
            "version" = "8.0.36"
          },
          {
            "description" = "8.0.37"
            "version" = "8.0.37"
          },
          {
            "description" = "8.0.38"
            "version" = "8.0.38"
          },
          {
            "description" = "8.0.39"
            "version" = "8.0.39"
          },
          {
            "description" = "8.0.40"
            "version" = "8.0.40"
          },
          {
            "description" = "8.0.41"
            "version" = "8.0.41"
          },
        ])
      },
      {
        "version_family" = "8.4 - LTS"
        "versions" = tolist([
          {
            "description" = "8.4.0"
            "version" = "8.4.0"
          },
          {
            "description" = "8.4.1"
            "version" = "8.4.1"
          },
          {
            "description" = "8.4.2"
            "version" = "8.4.2"
          },
          {
            "description" = "8.4.3"
            "version" = "8.4.3"
          },
          {
            "description" = "8.4.4"
            "version" = "8.4.4"
          },
        ])
      },
      {
        "version_family" = "9 - Innovation"
        "versions" = tolist([
          {
            "description" = "9.1.0"
            "version" = "9.1.0"
          },
          {
            "description" = "9.1.1"
            "version" = "9.1.1"
          },
          {
            "description" = "9.1.2"
            "version" = "9.1.2"
          },
          {
            "description" = "9.2.0"
            "version" = "9.2.0"
          },
        ])
      },
    ])

    获取特定大版本的各小版本

    可以通过 data资源中新增filter模块以过滤出需要的对象。

    在 Terraform 中,关于 data 资源是否可以使用 filter,以及filter支持的完整度视乎并没有明确的说明。这需要更具不同的供应商的实现。常见的,在data resourcefilter可以支持“列表匹配”、“通配符匹配”或者“正则匹配”。具体的匹配方式,则需要通过文档、或者测试区验证。

    添加带正则匹配的 filter
    data "oci_mysql_mysql_versions" "gmv" {
        #Required
        compartment_id = oci_identity_compartment.oic.id
        filter {
            name = "version_family"
            values = ["8.0.*"]
            regex  = true
        }
    }

    通过 HCL 语言获取最新的版本
    output "latest_versions" {
      value = {
        for db_version in data.oci_mysql_mysql_versions.gmv.versions : db_version.version_family => sort([
          for v in db_version.versions : v.version
        ])[length(db_version.versions) - 1] // 取排序后的最后一个版本
      }
    }

    最后的输出如下:

    latest_versions = {
      "8.0" = "8.0.41"
      "8.4 - LTS" = "8.4.4"
      "9 - Innovation" = "9.2.0"
    }

    参考链接

  • 快速了解 Aurora DSQL

    ·

    上周在 AWS re:Invent大会(类似于阿里云的云栖大会)上推出了新的产品 Aurora DSQL[1] ,在数据库层面提供了多区域、多点一致性写入的能力,兼容 PostgreSQL。并声称,在多语句跨区域的场景下,延迟只有Google Spanner的1/4。

    Aurora DSQL 提供了多可用区、多区域的多点一致性写入的内容。在技术层面,Aurora DSQL 通过把数据库的 log 模块和 block (或者说是cache)模块做了分离,从而更好的实现多点/多区域分布式能力,这与 Google AlloyDB 是比较类似的;此外,在跨区域强一致性实现上,则使用“Amazon Time Sync Service” [3] 来保障多个区域之间事务顺序的一致性。

    在产品层面,分为两个场景,一个是 Aurora DSQL(region内模式)和一个 Aurora DSQL Global 模式(多 region 内模式)。在 Region 内场景下,相比于普通 Aurora PostgreSQL ,Aurora DSQL 在多个可用区内都可以提供强一致的读写接入点,而Aurora PostgreSQL只在一个可用区提供写,其他可用区仅提供只读节点。

    在跨 Region 的场景下,Aurora DSQL 则提供了同步的、跨区域的多点写入能力。这对于业务在全球分布的客户,则可以进一步的降低业务的复杂度。而原来的 Aurora Global Database 仅提供单个 Region 的写入能力,并且,在其他 Region 的读节点需要承受一定的数据访问延迟,这对于很多的在线业务场景可能是无法接受的,或者需要在应用层面做针对性的改造。

    这是 Aurora 发布的10周年,AWS 依旧是创新、技术能力非常强的一家公司。此外,产品是在内测阶段,普通用户还无法体验。

    参考文档