介绍
word2vec中的logistic function计算使用的是快速算法,初始先算好值,然后在神经网络训练迭代中直接查表加快计算和训练速度。设计也还算精巧。
代码
在初始化时,有代码片段一,会对expTable做专门的初始化:
expTable数据的大小为1000,存了1000个值用于查表。
而在模型训练阶段,代码片段二:
将代码片段二代入到代码片段一,我们可以发现很神奇的现象:
将上式简单做个替换,把f替成z,整体输入看成i:
而再做一次运算,即知该表的值刚好与 logit函数 y = 1/(1+e^-z) 相同。
为什么要这么设计呢?
通过查表法,当然是为了加快指数运算。如果在每次迭代时,都去做指数运算,cpu开销蛮大。这样的设计,可以优化训练时间。
我来来看前前一段代码中,expTable[0 … EXP_TABLE_SIZE]中具体对应的值是哪些?这里,我提供了它的python实现:代码下载 ,绘制出实际的取值曲线(logistic regression的某一段取值),详见上面链接提供的代码:
另外,还有一点,就是这样的分割:EXP_TABLE_SIZE=1000, MAX_EXP=6? 把数字代入,即知:
\[e_{raw}=e^{(\frac{2*i}{1000}-1)*6}\] \[logit=\frac{e_{raw}}{e_{raw}+1}\] \[i=\frac{(f+6)*(1000)}{6*2}\]为什么要做这样的trick进行分解呢?上图展示的是从输入z的角度去理解。要真正理解为什么这样取,还要结合实际代码中Hierarchical softmax代码里的情况去理解。
里面的f输入范围在(-MAX_EXP, MAX_EXP), 即(-6, 6)。
第一阶段:(f + MAX_EXP)/MAX_EXP/2 * EXP_TABLE_SIZE 所做的操作:
- f + MAX_EXP: 平移,(0, 12)
- (f + MAX_EXP)/MAX_EXP: 压缩,(0,2)
- (f + MAX_EXP)/MAX_EXP/2: 归一化,(0,1)
- (f + MAX_EXP)/MAX_EXP/2 * EXP_TABLE_SIZE: 即将归一化后的f映射到[0,1000)个expTable的格子里,即是第一式的输入i
第二阶段:(i / (real)EXP_TABLE_SIZE * 2 - 1) * MAX_EXP 所做的操作:
- i / EXP_TABLE_SIZE: 将上面第一阶段的i,重新压缩到(0,1)
- i / (real)EXP_TABLE_SIZE * 2: 拉伸成(0,2)
- i / (real)EXP_TABLE_SIZE * 2 - 1: 平移为(-1,1)
- (i / (real)EXP_TABLE_SIZE * 2 - 1) * MAX_EXP: 放大为(-6,6)
ok,这时候,我们就会明白,为啥取(-6,6)了。因为:
- logit(6)=0.997527376843
- logit(-6)=0.00247262315663
这个范围内的值足够了。
但如果你细发现,两步合在一起,我们发现仿佛又回到了原点,于是可能开始会怀疑,为什么不干脆直接用f,省去中间这么多的变换,直接原封不动地输入expTable呢?
比如类似这样的实现:
我们可以推导下,如果让你设计一个需要1000个值的数组,存储logit函数值。假设数组名为expTable,即:
- 输入x为(0,1000)
- expTable[x]的输出值对应概率值(0,1)
由于logit(0)=0.5,常用的一个实现是:
\[y=\frac{1}{1+e^{-(x*2-1)}}\]从0开始刚好为0,输入(0,+inf),输出(0,1)。如果想让x刚好对应索引,刚在原基础上除以1000即可。即:
\[y=\frac{1}{1+e^{-(x*2/1000-1)}}\]再在原基础上做一下范围控制,就是我们上面的:
\[y=\frac{1}{1+e^{-(x*2/1000-1)*6}}\]如果希望得到更精准的值,将EXP_TABLE_SIZE和MAX_EXP调大些即可以。