问题分析
我们前面研究的都是基于统计的方法,通过不同的统计方法得到不同的准确率,通过改善统计的方式来提高准确率。现在我们要研究基于数学的方式来预测准确率。
假设我们有一个分词 s_{class,word},class是该对象的类别,word是该分词的词频。
则该分词在某类别中的分数为:s_(class) = sum(s{class,word})
那么我们欲求一个正确的类别:s_(class_g),如何使正确的类别的分数最高呢?
这里引入了损失函数
l
o
s
s
=
s
(
c
l
a
s
s
m
)
−
s
(
c
l
a
s
s
g
)
loss = s(class_m) - s(class_g)
loss=s(classm)−s(classg) 即用最大类别的分数 - 正确类别的分数。如果正确类别即是分数最大的类别,则loss为0;否则loss将为一个大于0的数。
如果loss函数越来越小,则说明正确的类别越来越多,准确率也越来越高。因此我们需要找到一个 s_(class),来使得loss最小。
一元函数的梯度下降法示例
图片来自吴恩达深度学习教程。
假定代价函数loss为J(w),则有
J
(
w
)
=
J
(
w
)
−
α
∗
d
J
(
w
)
d
w
J(w)=J(w)-α*\frac {dJ(w)}{dw}
J(w)=J(w)−α∗dwdJ(w)
α为学习率,用来控制每步的步长,即向下走一步的长度dJ(w)/dw就是函数J(w)对w求导
如图所示,该点的导数就是这个点相切于J(w)的小三角的高除以宽,如果我们从图中点为初始点开始梯度下降算法,则该点斜率符号为正,即dJ(w)/dw > 0,因此接下来会向左走一步。
整个梯度下降法的迭代过程就是不断向左走,直到逼近最小值点
若我们以如图点为初始化点,则该点处斜率为负,即dJ(w)/dw < 0,所以接下来会向右走:
整个梯度下降法的迭代过程就是不断向右走,即朝着最小值点的方向走。
梯度下降过程中有两个问题:
1.步长 不能过大,可能会跳过最小值点
答:设置学习率不断减小
2.局部最优解未必是全局最优解
答:参数量越大,找到全局最小值的概率越小,局部最小值就越接近全局最小值
基于梯度下降法的分类
所以我们可以有如下分析:
若class_max == class_g:
loss = 0
若class_max > class_g:
l
o
s
s
=
s
u
m
(
s
c
l
a
s
s
m
,
w
o
r
d
)
−
s
u
m
(
s
c
l
a
s
s
g
,
w
o
r
d
)
loss = sum(s_{class_m,word}) - sum(s_{class_g,word})
loss=sum(sclassm,word)−sum(sclassg,word)
则有
d
(
l
o
s
s
)
d
(
s
c
l
a
s
s
m
,
w
o
r
d
)
=
1
\frac {d( loss) }{d (s_{class_m,word})} = 1
d(sclassm,word)d(loss)=1
d
(
l
o
s
s
)
d
(
s
c
l
a
s
s
g
,
w
o
r
d
)
=
−
1
\frac{d( loss) }{d (s_{class_g,word})} = -1
d(sclassg,word)d(loss)=−1
我们将loss沿着导数方向移动:
s
c
l
a
s
s
m
,
w
o
r
d
−
=
l
r
∗
1
s_{class_m,word} -= lr*1
sclassm,word−=lr∗1
s
c
l
a
s
s
g
,
w
o
r
d
−
=
l
r
∗
(
−
1
)
s_{class_g,word} -= lr*(-1)
sclassg,word−=lr∗(−1)
化简则有:
s
c
l
a
s
s
m
,
w
o
r
d
−
=
l
r
s_{class_m,word} -= lr
sclassm,word−=lr
s
c
l
a
s
s
g
,
w
o
r
d
+
=
l
r
s_{class_g,word} += lr
sclassg,word+=lr
这会使得错误的s_{class_max,word}分数减小,使得正确的s_{class_g,word}分数增大,使得s(class_g)趋近于s(class_max),使loss函数值不断减小。
最初准确率会快速上升,这是调参的结果。当准确率逐渐稳定出现震荡时,我们得到的即为全局最优解,我们绕开统计的方法,找到一组权重求得最优解。
#learnw.py
#encoding: utf-8
import sys
from json import dump
from math import log, sqrt, inf
from random import uniform, seed
from tqdm import tqdm
def build_model(srcf, tgtf):
#{class: {word: freq}}
_c, _w = set(), set() #_c 为所有的类别,_w 为所有的词
with open(srcf,"rb") as fsrc, open(tgtf,"rb") as ftgt:
for sline, tline in zip(fsrc, ftgt):
_s, _t = sline.strip(), tline.strip() # s,t分别为句子和类别
if _s and _t:
_s, _class = _s.decode("utf-8"), _t.decode("utf-8")
if _class not in _c:#如果_c中没有这个类别
_c.add(_class)#添加到_c中
for word in _s.split():#遍历一行里面空格隔开的每个分词
if word not in _w:#如果_w中没有这个类别
_w.add(word)#添加到_w中
_ =sqrt( 2.0 / (len(_c) + len(_w))) #设置随机数,一定要小
return {_class: {_word: uniform(-_, _) for _word in _w} for _class in _c}
#为每个类别创建一个词典,词典中是子词和随机到的初始点w
def compute_instance(model, lin):
rs = {}
_max_score, _max_class = -inf, None
for _class, v in model.items(): #对模型中的每一类和类型的词典 v:{word: freq}
rs[_class] = _s = sum(v.get(_word, 0.0) for _word in lin) #这个类的分数即为该类中所有子词分数之和
if _s > _max_score:
_max_score = _s
_max_class = _class
#获取分数最高的类别
return rs, _max_class, _max_score#返回每个类别的分数、最大分数的类别、最大的分数
#返回每一类的频率分布,在这行中所有分词分数之和最大的类,最大的分数
def train(srcf, tgtf, model, base_lr=0.1, max_run=128):#学习率初始为0.1,训练128轮
with open(srcf,"rb") as fsrc, open(tgtf,"rb") as ftgt:
for _epoch in tqdm(range(1,max_run+1)): #在每轮中
fsrc.seek(0) #对文件做复位,每次读取文件开头
ftgt.seek(0)
_lr = base_lr / sqrt(_epoch) #将学习率不断变小,控制步长越来越小
total = err = 0
for sline, tline in zip(fsrc, ftgt):#对每行数据
_s, _t = sline.strip(), tline.strip()
if _s and _t:
_s, _class = _s.decode("utf-8").split(), _t.decode("utf-8")#_s为子词,_t为类别
scores, max_class, max_score = compute_instance(model, _s)#算出每行中每个类别的分数、最大分数的类别、最大的分数
if max_class != _class:#判断最大的类别和标准答案的类别是否相等
for _word in _s:
model[max_class][_word] -= _lr #错误的类别
model[_class][_word] += _lr #正确的类别
err += 1 #错误的数量+1
total += 1 #总条数+1
print("Epoch %d: %.2f" % (_epoch, (float(total - err) / total *100.0)))#返回每轮预测的准确率,转化为百分数
return model
def save(modin, frs):
with open(frs, "w") as f:
dump(modin, f) #用dump方法向文件写str
if __name__=="__main__":
seed(408) #固定随机数种子
save(train(*sys.argv[1:3], build_model(*sys.argv[1:3])),sys.argv[3])
#将第一个参数和第二个参数(训练集句子和分类)给build_model后,将句子、标签和返回的词典传入train中,最后保存模型
我们可以使用tqdm Python进度条来展示演示过程。在命令行执行:
:~/nlp/tnews$ python learnw.py src.train.bpe.txt tgt.train.s.txt learnw.model.txt
执行到100轮左右,准确率就在98%左右震荡。
对验证集做验证与早停
使用前面我们写好的脚本,在命令行输入:
:~/nlp/tnews$ python predict.py src.dev.bpe.txt learnw.model.txt pred.learnw.dev.txt
:~/nlp/tnews$ python acc.py pred.learnw.dev.txt tgt.dev.s.txt
46.92
可以看到,准确率是46.92%,在验证集上表现一般。与我们前面提到的统计方法相比,可以大致认为是统计方法最优化的最高准确率。
我们找到的参数是在训练集上表现最好的,但未必是在所有情况下表现最好的。我们需要的模型是在验证集上性能最好的,因此我们需要在模型对验证集的表现上做修改:
#encoding: utf-8
def eva(srcvf, tgtvf, model):#采集对验证集的正确率
total = corr = 0
with open(srcvf,"rb") as fsrc, open(tgtvf,"rb") as ftgt:
for sline, tline in zip(fsrc, ftgt):#对每行数据
_s, _t = sline.strip(), tline.strip()
if _s and _t:
_s, _class = _s.decode("utf-8").split(), _t.decode("utf-8")
max_class = compute_instance(model, _s)[1] #仅需要预测的最大类别
if max_class == _class:
corr += 1
total += 1
return float(corr) / total * 100.0 #返回每轮验证集的准确率
def train(srcf, tgtf, srcvf, tgtvf, model, frs, base_lr=0.1, max_run=128):#学习率初始为0.1,训练128轮
with open(srcf,"rb") as fsrc, open(tgtf,"rb") as ftgt:
_best_acc = eva(srcvf, tgtvf, model)
print("Epoch 0: dev %.2f" % _best_acc) #获得每轮中验证集上的准确率
for _epoch in range(1,max_run+1): #在每轮中
fsrc.seek(0) #对文件做复位,每次读取文件开头
ftgt.seek(0)
_lr = base_lr / sqrt(_epoch) #将学习率不断变小,控制步长越来越小
total = err = 0
for sline, tline in zip(fsrc, ftgt):#对每行数据
_s, _t = sline.strip(), tline.strip()
if _s and _t:
_s, _class = _s.decode("utf-8").split(), _t.decode("utf-8")#_s为子词,_t为类别
scores, max_class, max_score = compute_instance(model, _s)#算出每行中每个类别的分数、最大分数的类别、最大的分数
if max_class != _class:#判断最大的类别和标准答案的类别是否相等
for _word in _s:
model[max_class][_word] -= _lr #错误的类别
model[_class][_word] += _lr #正确的类别
err += 1 #错误的数量+1
total += 1 #总条数+1
_eva_acc = eva(srcvf, tgtvf, model)
print("Epoch %d: train %.2f, dev %.2f" % (_epoch, (float(total - err) /
total *100.0), _eva_acc)) #返回每轮训练集、验证集的准确率,转化为百分数
if _eva_acc >= _best_acc: #如果当前在验证集的准确率是最好的,就保存这个模型,并更新最好准确率
save(model, frs)
_best_acc = _eva_acc
return model
def save(modin, frs):
with open(frs, "w") as f:
dump(modin, f) #用dump方法向文件写str
if __name__=="__main__":
seed(408) #固定随机数种子
train(*sys.argv[1:5], build_model(*sys.argv[1:3]),sys.argv[5])
#传入训练句子、训练标签、验证句子、验证标签以及保存模型文件
在命令行执行:
:~/nlp/tnews$ python learnw.py src.train.bpe.txt tgt.train.s.txt src.dev.bpe.txt tgt.dev.s.txt learnw.model.txt
我们可以看到,对验证集的测试上,在很早的轮次已经趋于稳定值了。因此我们没有必要跑完所有128轮,对验证集的准确率的提升没有意义。
我们引入early_stop参数,作为早停的停止条件。如果我们连续early_stop轮都没有找到更好的结果,我们就将训练停止 ,避免无效的训练:
if _eva_acc >= _best_acc: #如果当前在验证集的准确率是最好的,就保存这个模型,并更新最好准确率
save(model, frs)
_best_acc = _eva_acc
anbest = 0
else:
anbest += 1
if anbest > earlystop: #如果连续earlystop轮准确率都没有提高,则停止训练
break