ULMFiT解读(论文 + PyTorch源码)

2025-08-18 09:37:36

可能是笔者孤陋寡闻,感觉这篇论文没有BERT、ELMo这么火,笔者也是在搜索相关话题的文章的时候,看到大家都会带着ULMFiT进行分析,因此也就去研究了一下。总体来说,这篇论文也是pretrain+finetune的思路,探索的比较浅,主要用来做文本分类,而且trick相对来说也比较多。但整体的思路比较值得借鉴。

文章目录

一. 前言

二. ULMFiT原理

1. 通用域语言模型pretrain

2. 目标域语言模型fineutune

3. 分类任务finetune

三. 实验

1. 分类任务实验

2. 一些分析

四. PyTorch实现

1. 语言模型pretrain

2. 语言模型finetune

3. 分类任务finetune

五. 总结

优势

不足

六. 一些思考

传送门

一. 前言

这里简单复述一下论文的作者在第一章中提到的贡献:

提出了ULMFiT(Universal Language Model Fine-tuning),用于实现像CV领域的迁移学习,并可以用于任意NLP任务。

提出了一些训练的策略,比如discriminative fine-tuning、slanted triangular learning rates、gradual unfreezing等。

在6个文本分类的任务上表现不俗,甚至提升了18~24%。

可以用少量样本训练。

重点来了!有充足的源码、预训练模型等。

二. ULMFiT原理

ULMFiT,根据它的名字,基本就可以知道它的操作流程,具体见下图:

一共是分为3个阶段,首先是语言模型的预训练、然后是语言模型的finetune、最后是分类任务的finetune。其实如果读者之前有过CV中图像分类的经验的话,可以发现这里面的后两步实际上都是finetune的操作,只不过这里将其分开进行叙述。下面将一一进行剖析:

1. 通用域语言模型pretrain

这一步没什么好说的,就是用了一个外部大数据(Wikitext-103,103 million词),先对LM进行pretrain。

2. 目标域语言模型fineutune

这一步的insight很直观,就是觉得通用域的语言模型数据会与目标域的数据有分布上的差别,所以要用目标域的语言数据先把LM finetune一波。这里就用到了两个trick:

discriminative fine-tuning

从名字上看,就是有区别性的finetune,在哪里有区别?论文中提到是在对不同层做finetune的时候,使用不同的学习率。作者通过经验发现,对于最后一层可先设置 n L n^L nL作为学习率,然后只训练最后一层,然后前面的层用 n l − 1 = n l / 2.6 n^{l-1} = n^l / 2.6 nl−1=nl/2.6继续训练。

slanted triangular learning rates(STLR)

这是一个学习率调整的方式,作者提到用这种方式的初衷是说,希望能先让参数较快收敛到一个合适的区域,然后再慢慢调整。所以他用这种类似三角的方式:

从图上直观来看长这样:

公式里面的 c u t cut cut就表示中间的那个尖对应的iteraion步数, T T T表示总的迭代步数, r a t i o ratio ratio就是一个比例参数, n m a x n_{max} nmax​是最大的学习率(就是尖对应的纵坐标)。一般取 c u t _ f r a c = 0.1 , r a t i o = 32 , n m a x = 0.01 cut\_frac = 0.1, ratio=32, n_{max} = 0.01 cut_frac=0.1,ratio=32,nmax​=0.01。

3. 分类任务finetune

这里就是将前面的LM输出进行concat,然后在其上加入两个全连接模块(带BN和ReLU激活的),进行分类即可。

具体地,对于LM的输出,将其最后一个隐层输出,与时间上的maxpool及meanpool进行concat:

h c = [ h T , m a x p o o l ( H ) , m e a n p o o l ( H ) ] h_c = [h_T, maxpool(H), meanpool(H)] hc​=[hT​,maxpool(H),meanpool(H)]

同时也提出了3个trick,用于更好的训练:

gradual unfreezing

其实就是在finetune的时候,逐层解冻前面的层。因为如果一次性finetune所有层的话,可能会出现灾难性遗忘(即训着训着就忘记了之前pretrain学到的东西),所以这里是逐层向前打开,逐渐加多finetune层数。

与这种方法相似的一个方法是"chain-thaw",这种方法是每次解冻一个层,每次也只训练那一个层,而不像这里,打开了过后,就一直训练下去。

BPT3C

主要是应对长文本的,将长文本分成batch个短句子,然后每次训练的时候,都是用前面一个batch的隐层状态进行初始化(这个好像也是LM训练的一个小trick),但是梯度不会传递到前面去。

双向语言模型

单独训练两个方向的语言模型,最后预测的结果是这两个的融合。

三. 实验

1. 分类任务实验

实验的任务主要是用在了文本分类上,有情感分类、问题分类、主题分类三大类。统计信息如下:

结果如下:

对比的模型都是他们写论文的时候SoTA的模型。

2. 一些分析

作者在论文里面做了很多有趣的分析,比如:

少数据量的学习

这个图是表示训练样本与验证集错误率的关系示意图,从左到右依次是IMDb、TREC-6和AG数据集。模型里面的From scratch表示完全从头开始训练,supervised表示仅用当前任务的数据进行LM的finetune,semi-supervised表示可以用所有task的数据进行LM的finetune。明显看出,用了较多数据进行finetune过后的LM,需要的训练样本更少,而且最终收敛效果也最好。

pretrain的影响

这个都不用多说了,直接看结果:

结论就是pretrain对于中小数据集来说,简直是救命,对于大数据集,也能提升表现。总之就是用就对了!

LM模型选择的影响

这里作者比较的是用最原始的LM和一个改进版本的好LM进行比较(据说是他们当时的SoTA):

显然好的LM,效果会更好。

finetune LM方式的影响

这部分就是验证2个trick的影响,结果如下:

这里证明了finetune LM的必要性,而且也证明了那两个trick非常好用!

finetune分类器方式的影响

这部分主要是对比一些trick使用的效果:

finetune分类器策略的稳定性

这部分主要是看了一下在finetune classifier的时候,直接finetune full model和用了trick的方式的对比,可见full的很不稳定。

双向模型的影响

一般双向融合都是能带来提升的。

四. PyTorch实现

ULMFiT在源码方面还是比较全面的,放出了论文中使用的所有脚本和详细的处理步骤,同时也提供了预训练好的模型,可以复现,也可以自己按照它那个步骤train自己想要的东西。下面笔者将按照论文中的三个步骤对相应的源码进行剖析:

1. 语言模型pretrain

语言模型的构建和训练部分比较简单,其代码如下:

# 构建模型

m = to_gpu(get_language_model(md.n_tok, em_sz, nh, nl, md.pad_idx, decode_train=False, dropouts=drops))

# 损失函数

crit = CrossEntDecoder(prs, m[1].decoder, n_neg=n_neg, sampled=sampled).cuda()

# 训练