ALBERT详解

BERT的问题

BERT 发布后,在排行榜上产生了许多 NLP 任务的最新成果。但是,模型非常大,导致了一些问题。"ALBERT"论文将这些问题分为两类:

内存限制

考虑一个包含一个输入节点,两个隐藏节点和一个输出节点的简单神经网络。即使是这样一个简单的神经网络,由于每个节点有权重和偏差,因此总共有7个参数需要学习

BERT-large是一个复杂的模型,它有24个隐藏层,在前馈网络和多头注意力机制中有很多节点,总共有3.4亿个参数,如果想要从零开始训练,需要花费大量的计算资源

模型退化

最近在NLP领域的研究趋势是使用越来越大的模型,以获得更好的性能。ALBERT的研究表明,无脑堆叠模型参数可能导致效果降低

在论文中,作者做了一个有趣的实验

如果更大的模型可以带来更好的性能,为什么不将最大的 BERT 模型(BERT-large)的隐含层单元增加一倍,从 1024 个单元增加到 2048 个单元呢?

他们称之为"BERT-xlarge"。令人惊讶的是,无论是在语言建模任务还是阅读理解测试(RACE)中,这个更大的模型的表现都不如BERT-large

从原文给出的图中(下图),我们可以看到性能是如何下降的

ALBERT

概述

ALBERT利用了参数共享、矩阵分解等技术大大减少了模型参数,用SOP(Sentence Order Prediction) Loss取代NSP(Next Sentence Prediction) Loss提升了下游任务的表现。但是ALBERT的层数并未减少,因此推理时间(Inference Time)还是没有得到改进。不过参数减少的确使得训练变快,同时ALBERT可以扩展到比BERT更大的模型(ALBERT-xxlarge),因此能得到更好的表现

ALBERT的结构和BERT基本一样,采用了Transformer以及GELU激活函数。具体的创新部分有三个:

  1. embedding层参数因式分解
  2. 跨层参数共享
  3. 将NSP任务改为SOP任务

前两个改进的主要作用是减少参数。第三个改进其实算不上什么创新了,因为之前已经有很多工作发现BERT中NSP任务并没有什么积极的影响

Factorized Embedding Parameterization

原始的BERT模型以及各种依据Transformer的预训连语言模型都有一个共同特点,即 E = H E=H E=H,其中 E E E指的是Embedding Dimension, H H H指的是Hidden Dimension。这就会导致一个问题,当提升Hidden Dimension时,Embedding Dimension也需要提升,最终会导致参数量呈平方级的增加。所以ALBERT的作者将** E E E H H H进行解绑**,具体的操作就是在Embedding后面加入一个矩阵进行维度变换 E E E的维度是不变的,如果 H H H增大了,我们只需要在E后面进行一个升维操作即可

所以,ALBERT不直接将原本的one-hot向量映射到hidden space size of H H H,而是分解成两个矩阵,原本参数数量为 V ∗ H V*H VH V V V表示的是Vocab Size。分解成两步则减少为 V ∗ E + E ∗ H V*E+E*H VE+EH,当 H H H的值很大时,这样的做法能够大幅降低参数数量

V ∗ H = 30000 ∗ 768 = 23 , 040 , 000 V*H=30000*768=23,040,000 VH=30000768=23,040,000

V ∗ E + E ∗ H = 30000 ∗ 256 + 256 ∗ 768 = 7 , 876 , 608 V*E+E*H=30000*256+256*768=7,876,608 VE+EH=30000256+256768=7,876,608

举个例子,当 V V V为30000, H H H为768, E E E为256时,参数量从2300万降低到780万

通过因式分解Embedding的实验可以看出,对于参数不共享的版本,随着 E E E的增大,效果是不断提升的。但是参数共享的版本似乎不是这样, E E E最大并不是效果最好。同时也能发现参数共享对于效果可能带来1-2个点的下降

Cross-Layer Parameter Sharing

传统Transformer的每一层参数都是独立的,包括各层的self-attention、全连接。这样就导致层数增加时,参数量也会明显上升。之前有工作试过单独将self-attention或者全连接层进行共享,都取得了一些效果。ALBERT作者尝试将所有层的参数进行共享,相当于只学习第一层的参数,并在剩下的所有层中重用该层的参数,而不是每个层都学习不同的参数

同时作者通过实验还发现了,使用参数共享可以有效的提升模型稳定性,实验结果如下图

BERT-base和ALBERT使用相同的层数以及768个隐藏单元,结果BERT-base共有1.1亿个参数,而ALBERT只有3100万个参数。通过实验发现,feed-forward层的参数共享会对精度产生比较大的影响;共享注意力参数的影响是最小的

Sentence-Order Prediciton (SOP)

BERT引入了一个叫做下一个句子预测的二分类问题。这是专门为提高使用句子对,如"自然语言推理"的下游任务的性能而创建的。但是像RoBERTa和XLNet这样的论文已经阐明了NSP的无效性,并且发现它对下游任务的影响是不可靠的

因此,ALBERT提出了另一个任务——句子顺序预测。关键思想是:

  • 从同一个文档中取两个连续的句子作为一个正样本
  • 交换这两个句子的顺序,并使用它作为一个负样本

SOP提高了下游多种任务(SQUAD,MNLI,SST-2,RACE)的表现

Adding Data & Remove Dropout

以上ALBERT都是使用跟BERT相同的训练数据。但是增加训练数据或许可以提升模型的表现,于是ALBERT加上STORIES Dataset后总共训练了157G的数据。另外,训练到1M步的时候,模型还没有对训练集Overfit,所以作者直接把Dropout移除,最终在MLM验证集上的效果得到了大幅提升

Conclusion

刚开始看这篇文章是很惊喜的,因为它直接把同等量级的BERT缩小了10+倍,让普通用户有了运行可能。但是仔细看了实验后才发现参数量的减小是需要付出代价的

需要注意的是,Speedup是训练时间而不是Inference时间。Inference时间并未得到改善,因为即使是使用了共享参数机制,还是得跑完12层Encoder,故Inference时间跟BERT是差不多的

实验用的参数如下

可以得出的结论是:

  1. 在相同的训练时间下,ALBERT得到的效果确实比BERT好
  2. 在相同的Inference时间下,ALBERT base和large的效果都没有BERT好,而且差了2-3个点,作者在最后也提到了会继续寻找提高速度的方法(Sparse attention和Block attention)

另外,结合Universal Transformer可以想到的是,在训练和Inference阶段可以动态地调整Transformer层数(告别12、24、48的配置)。同时可以想办法去避免纯参数共享带来的效果下降,毕竟Transformer中越深层学到的任务相关信息越多,可以改进Transformer模块,加入记忆单元、每层个性化的Embedding

Reference

### Python 函数使用教程 #### 1. 定义函数 在 Python 中,定义一个函数需要使用 `def` 关键字。函数名后面跟随括号和冒号,接着是从下一行开始缩进的函数体。 ```python def my_function(): print("Hello from a function") ``` 此代码创建了一个名为 `my_function` 的简单函数[^2]。 #### 2. 参数传递 Python 支持多种方式来向函数传入参数: - **位置参数**:按照顺序依次匹配形参。 ```python def greet(name, message): print(f"{message}, {name}!") greet('Alice', 'Good morning') ``` - **关键字参数**:通过指定名称的方式提供实参,允许改变调用时的实际参数顺序。 ```python greet(message='Good evening', name='Bob') ``` - **默认值参数**:如果未给定某个特定参数,则采用预先设定好的默认值。 ```python def say_hello(to="everyone"): print(f"Hello {to}") say_hello() ``` - **可变长度参数**:支持不定数量的位置参数或关键字参数。 对于任意多个非关键词参数,可以使用星号前缀的形式收集到元组中;而对于任意多的关键字参数则可以用双星号接收成字典形式。 ```python def make_pizza(*toppings): """打印顾客点的所有配料""" print("\nMaking pizza with the following toppings:") for topping in toppings: print("- " + topping) make_pizza('pepperoni') def build_profile(first, last, **user_info): profile = {} profile['first_name'] = first profile['last_name'] = last for key, value in user_info.items(): profile[key] = value return profile build_profile('albert', 'einstein', location='princeton', field='physics') ``` #### 3. 返回值 函数可以通过 `return` 语句返回处理后的数据给调用者。如果没有显式的 `return` 或仅执行了不带表达式的 `return` ,那么该函数会隐含地返回 None 值。 ```python def add(a, b): result = a + b return result sum_value = add(5,7) print(sum_value) # 输出 12 ``` #### 4. 局部变量与全局变量 局部变量是在函数内部声明并使用的变量,在离开作用域之后就会被销毁。而全局变量则是指在整个程序范围内都有效的变量。 当希望修改外部环境中的对象(如列表、字典),可以在函数体内直接操作这些容器类型的元素而不必担心它们会被丢弃掉。 但是要注意的是,默认情况下无法在一个函数里重新赋值给外面已存在的同名变量除非先加上 global 关键词声明它为全局变量。 ```python global_var = "I'm Global" def test_scope(): local_var = "I'm Local" print(local_var) test_scope() try: print(local_var) # 这将会引发 NameError 错误因为local_var只存在于test_scope这个环境中 except Exception as e: print(e) def modify_global(): global global_var global_var += ", modified inside function." modify_global() print(global_var) ``` #### 5. Lambda 表达式 Lambda 是一种简洁的方式来创建匿名的小型函数。这种语法非常适合用于那些只需要一次性的短小功能实现场景。 ```python double = lambda x : x * 2 multiply = lambda x,y:x*y print(double(5)) # 结果为 10 print(multiply(6,7)) # 结果为 42 ``` #### 6. 高阶函数 高阶函数是指能够接受其他函数作为输入参数或者把另一个函数当作输出结果的函数。常见的内置高阶函数有 map(), filter(), reduce() 等等。 map() 接受两个参数:一个是函数,另一个是要映射的数据序列。它会对每一个项目应用所提供的转换逻辑并将得到的新集合返回出来。 filter() 同样也带有这两个部分——筛选条件以及待过滤的对象集。不过这里所给出的操作应该是布尔判断性质的东西以便决定哪些成员应该保留下来形成最终的结果数组。 reduce() 则来自 functools 库,需单独导入才能正常使用。它的职责在于累积计算一系列数值之间的关系直至得出单一的答案为止。 ```python from functools import reduce numbers = [1, 2, 3] squared_numbers = list(map(lambda n:n*n , numbers)) even_numbers = list(filter(lambda n:(n%2==0), squared_numbers)) product_of_all_elements = reduce((lambda x, y: x * y), even_numbers) print(squared_numbers) # [1, 4, 9] print(even_numbers) # [4] print(product_of_all_elements)# 4 ``` #### 7. 递归函数 递归指的是函数在其自身的定义过程中直接或间接地调用了自己的一种编程技巧。为了防止无限循环的发生,通常会在每次迭代之前设置好终止条件。 下面是一个经典的例子展示了如何利用递归来解决斐波那契数列问题: ```python def fibonacci(n): if n <= 0: return "Input should be positive integer only!" elif n == 1 or n == 2 : return 1 else: return fibonacci(n-1)+fibonacci(n-2) for i in range(1,11): print(f"Fib({i})={fibonacci(i)}",end=', ') # Fib(1)=1, Fib(2)=1, Fib(3)=2, Fib(4)=3, Fib(5)=5, Fib(6)=8, Fib(7)=13, Fib(8)=21, Fib(9)=34, Fib(10)=55, ``` 此外还有二分查找算法也是典型的运用到了递归思想的应用案例之一.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数学家是我理想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值