SQL我们所知道的就是数据库查询语句,方便开发者对大数据进行高效的操作。而本文通过嵌套SQL查询语句,从另一个角度构建了一个简单的三层全连接网络。虽然语句嵌套太深,计算效率不高,但仍然是一个很有趣的实验。在本文中,我们将纯使用SQL实现一个具有一个隐藏层(以及具有ReLU和softmax的激活函数)的神经网络。这些神经网络训练步骤,包括正向传播和反向传播,将在BigQuery中的单个SQL查询语句中实现。当它在BigQuery中运行时,我们实际上是在数百或数千台服务器上进行分布式神经网络训练。听起来不错,对吧?也就是说,这个有趣的项目用于测试SQL和BigQuery的限制,同时从声明性数据的角度看待神经网络训练。该项目不考虑任何实际应用,但***我将讨论一些实际研究意义。让我们从一个基于神经网络的简单分类器开始。它接受大小为2的输入并输出二进制类。我们将有一个维度为2的隐藏层和一个ReLU激活函数。输出层的二元分类将使用softmax函数。我们在实施网络时遵循的步骤将基于Karpathy的CS231n指南(https://cs231n.github.io/neural-networks-case-study/)中提供的Python示例的SQL版本。模型该模型具有以下参数:隐藏层W的输入:2×2权重矩阵(元素:w_00、w_01、w_10、w_11)B:隐藏到输出层W2的2×1偏置向量(元素:b_0、b_1):2×2权重矩阵(元素:w2_00、w2_01、w2_10、w2_11)B2:2×1偏置向量(元素:b2_0、b2_1)训练数据存储在BigQuery表中,x1和x2列的输入和输出看起来像这样(表名:example_project.example_dataset.example_table)如前所述,我们将整个训练实现为单个SQL查询语句。训练完成后,参数的值会通过SQL查询语句返回。正如您可能猜到的那样,这将是一个嵌套查询,我们将逐步构建它来准备此查询。我们将从最内层的子查询开始,逐层递增到嵌套的外层。前向传播首先,我们将权重参数W和W2设置为服从正态分布的随机值,将权重参数B和B2设置为0。W和W2的随机值可以由SQL自己生成。为简单起见,我们将在外部生成这些值并在SQL查询中使用它们。使用开始化参数的内部子查询如下:SELECT*,-0.00569693ASw_00,0.00186517ASw_01,0.00414431ASw_10,0.0105101ASw_1ASw_11,0,0.0105101ASw_00,0.00186517ASw_10,-0.01312284ASw2_00,-0.01269512ASw2_01,0.00379152ASw2_10,-0.01218354ASw2_11,0.0ASb2_0,0.0ASb2_1FROM`example_project.example_dataset.example_table`注意表example_project.example.example2已经包含列x.dataset.example1模型参数将作为附加列添加到上述查询结果中。接下来,我们将计算隐藏层的激活值。我们将隐藏层表示为具有元素d0和d1的向量D。我们需要执行矩阵运算D=np.maximum(0,np.dot(X,W)+B),其中X表示输入向量(元素x1和x2)。该矩阵运算包括将权重W乘以输入X,再加上偏置向量B。然后,结果将传递给非线性ReLU激活函数,该函数会将负值设置为0。SQL中的等效查询是:SELECT*,(CASEWHEN((x1*w_00+x2*w_10)+b_0)>0.0THEN((x1*w_00+x2*w_10)+b_0)ELSE0.0END)ASd0,(CASEWHEN((x1*w_01+x2*w_11)+b_0)>0.0THEN((x1*w_01+x2*w_11)+b_1)ELSE0.0END)ASd1FROM{innersubquery}上面的查询在结果中添加了两个新列d0和d1以前的内部子查询。上述查询的输出如下所示。这样就完成了输入层到隐藏层的转换。现在,我们可以执行从隐藏层到输出层的转换。首先,我们将计算输出层的值。公式为:scores=np.dot(D,W2)+B2。然后,我们将对计算值应用softmax函数以获得每个类别的预测概率。SQL中的等效子查询如下:SELECT*,EXP(scores_0)/(EXP(scores_0)+EXP(scores_1))ASprobs_0,EXP(scores_1)/(EXP(scores_0)+EXP(scores_1))ASprobs_1FROM(SELECT*,((d0*w2_00+d1*w2_10)+b2_0)ASscores_0,((d0*w2_01+d1*w2_11)+b2_1)ASscores_1FROM{INNERsub-query})首先,我们将使用交叉熵损失函数来计算当前预测的总损失。首先,计算每个样本中正确类别的预测概率对数的负数。交叉熵损失只是这些X和Y实例中值的平均值。自然对数是递增函数,因此将损失函数定义为正确类别的预测概率的负对数是直观的。如果正确类别的预测概率很高,损失函数就会很低。相反,如果正确类别的预测概率较低,则损失函数值会很高。为了降低过度拟合的风险,我们还将添加L2正则化。在整体损失函数中,我们将包括0.5*reg*np.sum(W*W)+0.5*reg*np.sum(W2*W2),其中reg是一个超参数。将这个函数包含在损失函数中,会对权重向量中那些较大的值进行惩罚。在查询中,我们还计算了训练示例的数量(num_examples)。这在我们计算平均值时会有用。SQL查询中计算整体损失函数的语句如下:SELECT*,(sum_correct_logprobs/num_examples)+1e-3*(0.5*(w_00*w_00+w_01*w_01+w_10*w_10+w_11*w_11)+0.5*(w2_00*w2_00+w2_01*w2_01+w2_10*w2_10+w2_11*w2_11))ASlossFROM(SELECT*,SUM(correct_logprobs)OVER()sum_correct_logprobs,COUNT(1)OVER()num_examplesFROM(SELECT*,(CASEWHENy=0THEN)-1*LOG(probs_0)ELSE-1*LOG(probs_1)END)AScorrect_logprobsFROM{innersubquery}))反向传播接下来,对于反向传播,我们将计算每个参数相对于损失函数的偏导数。我们使用链式法则从最后一层开始逐层计算。首先,我们将使用交叉熵和softmax函数的导数来计算分数的梯度。相反的查询是:SELECT*,(CASEWHENy=0THEN(probs_0–1)/num_examplesELSEprobs_0/num_examplesEND)ASdscores_0,(CASEWHENy=1THEN(probs_1–1)/num_examplesELSEprobs_1/num_examplesEND)ASdscores_1FROM{innersubquery}以上,我们使用scores=np.dot(D,W2)+B2计算分数。因此,根据分数的偏导数,我们可以计算隐藏层D的梯度和参数W2、B2。对应的查询语句为:SELECT*,SUM(d0*dscores_0)OVER()ASdw2_00,SUM(d0*dscores_1)OVER()ASdw2_01,SUM(d1*dscores_0)OVER()ASdw2_10,SUM(d1*dscores_1)OVER()ASdw2_11,SUM(dscores_0)OVER()ASdb2_0,SUM(dscores_1)OVER()ASdb2_1,CASEWHEN(d0)<=0.0THEN0.0ELSE(dscores_0*w2_00+dscores_1*w2_01)ENDASdhidden_??0,CASEWHEN(d1)<=0.0THEN0.0ELSE(dscores_0*w2_10+dscores_1*w2_11)ENDASdhidden_??1FROM{innersubquery}同样,我们知道D=np.maximum(0,np.dot(X,W)+B)。因此,利用D的偏导数,我们可以计算W和B的导数。我们不需要计算X的偏导数,因为它不是模型的参数,也不必是与其他模型参数一起计算。计算W和B的偏导数的查询语句如下:SELECT*,SUM(x1*dhidden_??0)OVER()ASdw_00,SUM(x1*dhidden_??1)OVER()ASdw_01,SUM(x2*dhidden_??0)OVER()ASdw_10,SUM(x2*dhidden_??1)OVER()ASdw_11,SUM(dhidden_??0)OVER()ASdb_0,SUM(dhidden_??1)OVER()ASdb_1FROM{innersubquery}***,我们分别使用W,B,W2,B2的导数用于更新操作。计算公式为param=learning_rate*d_param,其中learning_rate为参数。为了体现L2正则化,我们会在计算dW和dW2的时候加入一个正则项reg*weight。我们还删除了缓存列,如dw_00、correct_logprobs等,它们是在子查询期间创建的,用于保存训练数据(列x1、x2和y)和模型参数(权重和偏差)。对应的查询语句如下:SELECTx1,x2,y,w_00—(2.0)*(dw_00+(1e-3)*w_00)ASw_00,w_01—(2.0)*(dw_01+(1e-3)*w_01)ASw_01,w_10—(2.0)*(dw_10+(1e-3)*w_10)ASw_10,w_11—(2.0)*(dw_11+(1e-3)*w_11)ASw_11,b_0—(2.0)*db_0ASb_0,b_1—(2.0)*db_1ASb_1,w2_00—(2.0)*(dw2_00+(1e-3)*w2_00)ASw2_00,w2_01—(2.0)*(dw2_01+(1e-3)*w2_01)ASw2_01,w2_10—(2.0)*(dw2_10+(1e-3))*w2_10)ASw2_10,w2_11—(2.0)*(dw2_11+(1e-3)*w2_11)ASw2_11,b2_0—(2.0)*db2_0ASb2_0,b2_1—(2.0)*db2_1ASb2_1FROM{innersubquery}这包括正向和反向传播a整个迭代过程。上面的查询将返回更新后的权重和偏差。部分结果如下所示:对于多次训练迭代,我们将重复上述过程。一个简单的Python函数就够了,代码链接如下:https://github.com/harisankarh/nn-sql-bq/blob/master/training.py。由于迭代次数太多,导致查询语句嵌套严重。执行10次训练迭代的查询语句地址如下:https://github.com/harisankarh/nn-sql-bq/blob/master/out.txt由于查询语句的多重嵌套和复杂性,在BigQuery中执行查询多个系统资源处于临界状态。BigQuery的标准SQL扩展比传统的SQL语言具有更好的扩展性。对于具有100k个实例的数据集,即使是标准的SQL查询也很难执行超过10次迭代。由于资源限制,我们将使用一个简单的决策边界来评估模型,这样我们可以在少量迭代后获得良好的准确性。我们将使用一个简单的数据集,其输入X1、X2服从标准正态分布。二进制输出y只是检查x1+x2是否大于0。为了更快地训练10次迭代,我们使用更大的学习率2.0(注意:这么大的学习率不建议实际使用,可能会导致发散).上述语句执行10次迭代得到的模型参数如下:我们将使用Bigquery的savetotable函数将结果保存到新表中。我们现在可以对训练集进行推理,以比较预测值和预期值。查询语句片段在以下链接:https://github.com/harisankarh/nn-sql-bq/blob/master/query_for_prediction.sql。在仅仅十次迭代中,我们就达到了93%的准确率(在测试集上也差不多)。如果我们将迭代次数增加到100,准确率高达99%。优化以下是该项目的摘要。我们从中得到了什么启示?如您所见,资源瓶颈决定了数据集的大小和执行的迭代次数。除了祈求谷歌开放资源上限,我们还有以下优化方法来解决这个问题。创建中间表和多个SQL语句可以帮助增加迭代次数。例如,前10次迭代的结果可以存储在中间表中。同一查询的下10次迭代可以基于此中间表。因此,我们进行了20次迭代。对于较大的查询迭代,可以重复使用此方法。我们应该尽可能使用函数嵌套,而不是在每一步都添加外部查询。例如,在一个子查询中,我们可以同时计算scores和probs,而不是使用2级嵌套查询。在上面的示例中,所有中间项都保留到执行最后一个外部查询为止。其中一些项目,例如correct_logprobs,可以更早地删除(尽管SQL引擎可能会自动执行此类优化)。更多尝试应用用户定义的函数。如果您有兴趣,可以查看这个BigQuery项目,用于为具有用户定义函数的模型提供服务(但是,您不能使用SQL或UDF进行训练)。意义现在,让我们看一下基于深度学习的分布式SQL引擎的深层意义。像BigQuery、Presto这样的SQL仓库引擎的一个限制是查询操作是在CPU而不是GPU上执行的。研究blazingdb、mapd等GPU加速数据库的查询结果一定很有意思。一种简单的研究方法是使用分布式SQL引擎进行查询和数据分发,并使用GPU加速的数据库进行本地计算。退后一步,我们已经知道执行分布式深度学习是困难的。分布式SQL引擎几十年来有大量的研究工作,产生了今天的查询计划、数据分区、操作放置、检查点和多查询调度等技术。其中一些可以与分布式深度学习相结合。如果您对这些感兴趣,请查看这篇论文(https://sigmodrecord.org/publications/sigmodRecord/1606/pdfs/04_vision_Wang.pdf),其中提供了对分布式数据库和分布式深度学习研究讨论的广泛概述.原文链接:https://towardsdatascience.com/deep-neural-network-implemented-in-pure-sql-over-bigquery-f3ed245814d3
