深度学习公式 向量化

602 阅读4分钟

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战


Σ( ° △ °|||)︴本篇文章要讲如何对公式进行向量化来简化计算,可能会穿插一切废话,都用引用符号标记出来了,可以不看。

如何抛开人的主观逻辑思考?怎么用更适合计算机的角度分析问题?

可能很多人不不理解我这个问题,那我举个栗子。因为我本科是学生物的,本科时期计算机算是辅修。起初我就毫无这种简化问题的思维。

比如一个简单的十进制转二进制。在我眼里,逻辑上可以算作有两种方法:

方法一:看看这个数是2的几次方的和

819=1×29+1×28+0×27+0×26+1×25+1×24+0×23+0×22+1×21+1×20819 = 1 \times 2^9 + 1 \times 2^8 + 0 \times 2^7 + 0 \times 2^6 + 1 \times 2^5 + 1 \times 2^4 + 0 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 1 \times 2^0

方法二:除留取余法

819÷2=409...1819 \div 2 = 409 ... 1

409÷2=204...1409 \div 2 = 204 ... 1

204÷2=102...0204 \div 2 = 102 ... 0

102÷2=51.....0102 \div 2 = 51 ..... 0

51÷2=25.......151 \div 2 = 25 ....... 1

25÷2=12.......125 \div 2 = 12 ....... 1

12÷2=6.........012 \div 2 = 6 ......... 0

6÷2=3...........06 \div 2 = 3 ........... 0

3÷2=1...........13 \div 2 = 1 ........... 1

1÷2=0...........11 \div 2 = 0 ........... 1

当时在我眼里,我觉得怎么看都是方法一更简单,看看是二的几次方就行了。直到我一次刷题遇到进制转换突然蒙了……咦?这怎么办唉。除2的几次方这个逻辑怎么写唉。难道搞两个数组,一个存2的n次方,一个存除数结果吗?

然后我去求助,计算机的学长说你就学了这一种方法?我说不啊我还会别的。他说那你用另个方法写一下就好了。然后我发现用除留取余法明显更简单。

这就是我说的更适合计算机分析的角度处理问题,也许我主管认为某种方法简单,但是对于计算机来讲可能另一种处理方法更便于实现,所以对于一些问题,我们要转换思路,找到更适合计算机处理的方法来进行。

所以对于深度学习,“复杂的公式”如何简化计算?

举个简单栗子:

hθ(x)=j=0nθjxjh_{\theta}(x)=\sum_{j=0}^{n} \theta_{j} x_{j}

如果让你自己手算这个,你肯定会想,不就是个简单的一次函数吗,挨个带进去算就行了。但是如果“带进去算”这个方法让计算机来实现就会变成这样:

声明两个向量,遍历。

Unvectorized implementation(没向量化)

C++:

double prediction = 0.0; 
for(int j = 0; j<=n; j++)
{
    prediction += theta[j]*x[j];
}

octave:

prediction =0.0for j = 1:n+l
    prediction = prediction + theta(j) * x(j)
end

上边提到了“没向量化”。什么是向量化呢? 就是将你认知上的公式转化成矩阵和向量的乘法。这样计算机处理起来就会更迅速,也就是我前边提到的“更适合计算机的角度去处理问题”。

hθ(x)=j=0nθjxjh_{\theta}(x)=\sum_{j=0}^{n} \theta_{j} x_{j} 的向量化可以将其看做是θTx\theta^{T} x而进行计算。

这样就转化为两个向量θ=[θ0θ1θ2...]x=[x0x1x2...]\theta=\left[\begin{array}{l}\theta_{0} \\ \theta_{1} \\ \theta_{2} \\ ... \end{array}\right] \quad x=\left[\begin{array}{l}x_{0} \\ x_{1} \\ x_{2} \\ ...\end{array}\right]的乘积了。

Vectorized implementation(向量化之后)

octave:

prediction= theta' * x; 

C++:

double prediction = theta.transpose()*x;

向量化之后为什么比你自己用数组计算迅速呢?因为向量化之后可以更便捷的用到一些语言内置的库。比如上边octave中的theta'英文单引号 ' 表示求矩阵或向量的转置(忘记的回去看octave语法)。C++中theta.transpose()也是用到了线性代数库的转置函数。

作为一个学生物的,我是用C++作为入门语言的,刚开始学数据结构和算法的时候,比如排序,最然我了解各种排序及其效率。但是如果问我给出一组什么特征的数据用哪种排序算法效率高,我才会考虑用哪个更好,否则刷题时候我一般都是直接冒泡。有次学长跟我说:你说C++库函数用的是哪个排序。我不知道。然后他说:你可以闲着没事去看看库函数源代码,比如C++的排序,不是单一某个排序而是排序的组合体,都是各种大佬研究优化出来的算法。

使用高级语言的库,更便捷了我们的操作。这些库函数都是各种计算机的大佬创造优化出来的,比我们自己写便捷千百倍。

用内置算法的好处:

  • 速度更快
  • 用更少代码实现
  • 相比于你自己写的更不易出错
  • 更好地配合硬件系统

说了这么多,再来个栗子:

θj:=θjα1mi=1m(hθ(x(i))y(i))xj(i) for all j\begin{array}{l} \theta_{j}:=\theta_{j}-\alpha \frac{1}{m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right) x_{j}^{(i)} \end{array} \text{ for all j}

这个公式很熟悉,是梯度下降的公式。 在这个公式中x是数据矩阵,y是列向量。

对于多元线性回归:

θ0:=θ0α1mi=1m(hθ(x(i))y(i))x0(i)θ1:=θ1α1mi=1m(hθ(x(i))y(i))x1(i)θ2:=θ2α1mi=1m(hθ(x(i))y(i))x2(i)...\begin{array}{l} \theta_{0}:=\theta_{0}-\alpha \frac{1}{m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right) x_{0}^{(i)} \\ \theta_{1}:=\theta_{1}-\alpha \frac{1}{m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right) x_{1}^{(i)} \\ \theta_{2}:=\theta_{2}-\alpha \frac{1}{m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right) x_{2}^{(i)} \end{array} \\...

先简化一下梯度下降公式使其更容易编写代码:

θ:=θαδ\theta:=\theta-\alpha \delta
δ=1mi=1m(hθ(x(i))y(i))x(i)\delta = \frac{1}{m} \sum_{i=1}^{m}\left(h_{\theta}\left(x^{(i)}\right)-y^{(i)}\right) x^{(i)}

其中δ是个列向量:δ=[δ0δ1δ2...]\delta=\left[\begin{array}{l} \delta_{0} \\ \delta_{1} \\ \delta_{2} \\... \end{array}\right]

而对于x来说,x(i)=[x0(i)x1(i)x2(i)...]x^{(i)}=\left[\begin{array}{c} x_{0}^{(i)} \\ x_{1}^{(i)} \\ x_{2}^{(i)} \\... \end{array}\right]

δ=×(...)×x(i)\delta = 数×(...)× x^{(i)}

由此可是一定是 δn×1=×(...)×x1×n(i)\delta_{n \times 1} = 数×(...)× x_{1×n}^{(i)}xj(i)x_j^{(i)}进行转置。

这个逻辑如果不向量化的话可能需要写好多循环才能完成。 我随手用c++写了一下,不保证对啊,你们大致看一下就行了,我也没运行这段代码。

for(int j=0;j<n;j++)
{
    //先计算出h(x)来
    for(int i=0;i<len;i++)
    {
        hx[i] += theta[i][j]*x[i][j];
    }
    //计算[h(x)-j]*x并求和
    for(int i=0;i<len;i++)
    {
        sum += (h[i] - y[i])*x[i][j]; 
    }
    //公式剩余部分
    theta[j] = theta[j] - alpha * (1/m) * sum;
}

但是如果你向量化以后就可以写为:

% 假设现在是二元的
hx = X * theta;
theta(1) = theta(1) - alpha * (1/m) * sum((hx-y)*X(:,1:1))
theta(2) = theta(2) - alpha * (1/m) * sum((hx-y)*X(:,2:2))
%注意octave和C等其他语言不同,下标从1开始
% 如果是n元的
hx = X * theta;
for i = 1:n
    theta(i) = theta(i) - alpha * (1/m) * sum((hx-y)*X(:,i:i))
    %注意octave和C等其他语言不同,下标从1开始
endfor

向量化之后怎么都好处理,但是如果不经过向量化,那你可能就要嵌套好多for循环了。所以要善用向量化减少工作量。