乾物丨DolphinDB一般的なコンピューティングチュートリアル

DolphinDBは、分散データを保存できるだけでなく、分散コンピューティングも適切にサポートしています。DolphinDBでは、ユーザーはシステムが提供する一般的な分散コンピューティングフレームワークを使用して、特定の基盤となる実装に注意を払うことなく、スクリプトを介して効率的な分散アルゴリズムを実装できます。この記事では、DolphinDBの一般的なコンピューティングフレームワークの重要な概念と関連機能について詳しく説明し、具体的な使用シナリオと例を豊富に提供します。


1.データソース

データソースは、DolphinDBの一般的なコンピューティングフレームワークの基本概念です。これは、データのメタ記述である特殊なタイプのデータオブジェクトです。データソースを実行することにより、ユーザーはテーブル、行列、ベクトルなどのデータエンティティを取得できます。DolphinDBの分散コンピューティングフレームワークでは、巨大なデータエンティティの代わりに軽量のデータソースオブジェクトが後続の計算のためにリモートノードに送信されるため、ネットワークトラフィックが大幅に削減されます。

DolphinDBでは、ユーザーはSQLDS関数を使用して、SQL式に基づいてデータソースを生成することがよくあります。この関数はテーブルを直接クエリしませんが、1つ以上のSQLサブクエリ、つまりデータソースのメタステートメントを返します。その後、ユーザーはMap-Reduceフレームワークを使用して、データソースと計算関数を渡し、各データソースに対応するノードにタスクを分散し、計算を並行して完了して、結果を要約できます。

データソースを取得するために一般的に使用されるいくつかの方法については、この記事のセクション3.1、3.2、3.3、および3.4​​で詳しく説明します。


2.Map-Reduceフレームワーク

Map-Reduce関数は、DolphinDBの一般的な分散コンピューティングフレームワークのコア関数です。

2.1mr関数

DolphinDBのMap-Reduce関数mrの構文はmr(ds、mapFunc、[reduceFunc]、[finalFunc]、[parallel = true])であり、データソースのセットとmapFunc関数をパラメーターとして受け入れます。各データソースが配置されているノードにコンピューティングタスクを分散し、mapFuncを介して各データソースのデータを処理します。オプションのパラメーターreduceFuncは、mapFuncの戻り値をペアごとに計算し、得られた結果は3番目のmapFuncの戻り値で計算されるため、累積計算ではmapFuncの結果が要約されます。M個のマップ呼び出しがある場合、reduce関数はM-1回呼び出されます。オプションのパラメーターfinalFuncは、reduceFuncの戻り値をさらに処理します。

公式文書には、mrを介して分散最小二乗線形回帰を実行する例がありますこの記事では、次の例を使用して、mr呼び出しを使用して、分散テーブルの各パーティションのデータの10分の1をランダムにサンプリングする方法を示します。

//データベースとDFSテーブルを作成します
db = database( "dfs:// sampleDB"、VALUE、 `a`b`c`d)
t = db.createPartitionedTable(table(100000:0、` sym`val、[SYMBOL、 DOUBLE])、 `tb、` sym)
n = 3000000 
t.append!(table(rand( `a`b`c`d、n)as sym、rand(100.0、n)as val))

//マップを定義関数
defsampleMap(t){ 
    sampleRate = 0.1 
    rowNum = t.rows()
    sampleIndex =(0 ..(rowNum-1))。shuffle()[0:int(rowNum * sampleRate)] 
    return t [sampleIndex] 
} 

ds = sqlDS(<select * from t>)//データソースを作成します
res = mr(ds、sampleMap ,, unionAll)//計算を実行します

上記の例では、ユーザー定義のsampleMap関数は、テーブル(つまり、データソース内のデータ)をパラメーターとして受け取り、行の10分の1をランダムに返します。この例のmr関数にはreduceFuncパラメーターがないため、各マップ関数の戻り値はタプルに配置され、finalFunc、つまりunionAllに渡されます。unionAllは、map関数によって返された複数のテーブルを、順次パーティションを持つ分散テーブルにマージします。

2.2imr関数

DolphinDBデータベースは、Map-Reduceメソッドに基づく反復計算関数imrを提供します。mrと比較すると、反復計算をサポートできます。各反復は、前の反復の結果と入力データセットを使用するため、より複雑なアルゴリズムの実装をサポートできます。反復計算には、モデルパラメータの初期値と終了基準が必要です。その構文はimr(ds、initValue、mapFunc、[reduceFunc]、[finalFunc]、terminateFunc、[carryover = false])です。ここで、initValueパラメーターは最初の反復の初期値であり、mapFuncパラメーターは関数であり、受け入れられます。パラメータには、データソースエンティティ、および前の反復での最終関数の出力が含まれます。最初の反復では、これはユーザーが指定した初期値です。imrのパラメーターは、mr関数に似ています。finalFunc関数は2つのパラメーターを受け入れます。最初のパラメーターは、前の反復での最後の関数の出力です。最初の反復では、これはユーザーが指定した初期値です。2番目のパラメーターは、reduce関数を呼び出した後の出力です。terminalFuncパラメーターは、反復が終了するかどうかを判別するために使用されます。2つのパラメータを受け入れます。1つ目は、前の反復でのreduce関数の出力であり、2つ目は、現在の反復でのreduce関数の出力です。trueが返された場合、反復は中止されます。キャリーオーバーパラメーターは、map関数呼び出しが次のmap関数呼び出しに渡されるオブジェクトを生成するかどうかを示します。キャリーオーバーがtrueの場合、map関数には3つのパラメーターがあり、最後のパラメーターはキャリーオブジェクトであり、map関数の出力結果はタプルであり、最後の要素はキャリーオブジェクトです。最初の反復では、運ばれるオブジェクトはNULLです。

公式文書には、imrを介して分散データの中央値を計算する例がありますこの記事では、より複雑な例を示します。つまり、ニュートン法を使用してロジスティック回帰(ロジスティック回帰)計算を実行し、機械学習アルゴリズムでのimrのアプリケーションを示します。

def myLrMap(t, lastFinal, yColName, xColNames, intercept) {
	placeholder, placeholder, theta = lastFinal
    if (intercept)
        x = matrix(t[xColNames], take(1.0, t.rows()))
    else
        x = matrix(t[xColNames])
    xt = x.transpose()
    y = t[yColName]
    scores = dot(x, theta)
    p = 1.0 \ (1.0 + exp(-scores))
    err = y - p
    w = p * (1.0 - p)
    logLik = (y * log(p) + (1.0 - y) * log(1.0 - p)).flatten().sum()
    grad = xt.dot(err)                   // 计算梯度向量
    wx = each(mul{w}, x)
    hessian = xt.dot(wx)                 // 计算Hessian矩阵
    return [logLik, grad, hessian]
}

def myLrFinal(lastFinal, reduceRes) {
    placeholder, placeholder, theta = lastFinal
    logLik, grad, hessian = reduceRes
    deltaTheta = solve(hessian, grad)    // deltaTheta等于hessian^-1 * grad,相当于解方程hessian * deltaTheta = grad
    return [logLik, grad, theta + deltaTheta]
}

def myLrTerm(prev, curr, tol) {
	placeholder, grad, placeholder = curr
	return grad.flatten().abs().max() <= tol
}

def myLr(ds, yColName, xColNames, intercept, initTheta, tol) {
    logLik, grad, theta = imr(ds, [0, 0, initTheta], myLrMap{, , yColName, xColNames, intercept}, +, myLrFinal, myLrTerm{, , tol})
    return theta
}

在上述例子中,map函数为数据源中的数据计算在当前的系数下的梯度向量和Hessian矩阵;reduce函数将map的结果相加,相当于求出整个数据集的梯度向量和Hessian矩阵;final函数通过最终的梯度向量和Hessian矩阵对系数进行优化,完成一轮迭代;terminate函数的判断标准是本轮迭代中梯度向量中最大分量的绝对值是否大于参数tol。

这个例子还可以通过数据源转换操作,进一步优化以提高性能,具体参见3.6节。

作为经常使用的分析工具,分布式逻辑回归已经在DolphinDB中作为内置函数实现。内置版本(logisticRegression)提供更多功能。


3. 数据源相关函数

DolphinDB提供了以下常用的方法获取数据源:

3.1 sqlDS函数

sqlDS函数根据输入的SQL元代码创建一个数据源列表。 如果SQL查询中的数据表有n个分区,sqlDS会生成n个数据源。 如果SQL查询不包含任何分区表,sqlDS将返回只包含一个数据源的元组。

sqlDS是将SQL表达式转换成数据源的高效方法。用户只需要提供SQL表达式,而不需要关注具体的数据分布,就能利用返回的数据源执行分布式算法。下面提供的例子,展示了利用sqlDS对DFS表中的数据执行olsEx分布式最小二乘回归的方法。

// 创建数据库和DFS表
db = database("dfs://olsDB", VALUE, `a`b`c`d)
t = db.createPartitionedTable(table(100000:0, `sym`x`y, [SYMBOL,DOUBLE,DOUBLE]), `tb, `sym)
n = 3000000
t.append!(table(rand(`a`b`c`d, n) as sym, 1..n + norm(0.0, 1.0, n) as x, 1..n + norm(0.0, 1.0, n) as y))

ds = sqlDS(<select x, y from t>)    // 创建数据源
olsEx(ds, `y, `x)                   // 执行计算


3.2 repartitionDS函数

sqlDS的数据源是系统自动根据数据的分区而生成的。有时用户需要对数据源做一些限制,例如,在获取数据时,重新指定数据的分区以减少计算量,或者,只需要一部分分区的数据。repartitionDS函数就提供了重新划分数据源的功能。

函数repartitionDS根据输入的SQL元代码和列名、分区类型、分区方案等,为元代码生成经过重新分区的新数据源。

以下代码提供了一个repartitionDS的例子。在这个例子中,DFS表t中有字段deviceId, time, temperature,分别为symbol, datetime和double类型,数据库采用双层分区,第一层对time按VALUE分区,一天一个分区;第二层对deviceId按HASH分成20个区。

现需要按deviceId字段聚合查询95百分位的temperature。如果直接写查询select percentile(temperature,95) from t group by deviceID,由于percentile函数没有Map-Reduce实现,这个查询将无法完成。

一个方案是将所需字段全部加载到本地,计算95百分位,但当数据量过大时,计算资源可能不足。repartitionDS提供了一个解决方案:将表基于deviceId按其原有分区方案HASH重新分区,每个新的分区对应原始表中一个HASH分区的所有数据。通过mr函数在每个新的分区中计算95百分位的temperature,最后将结果合并汇总。

// 创建数据库
deviceId = "device" + string(1..100000)
db1 = database("", VALUE, 2019.06.01..2019.06.30)
db2 = database("", HASH, INT:20)
db = database("dfs://repartitionExample", COMPO, [db1, db2])

// 创建DFS表
t = db.createPartitionedTable(table(100000:0, `deviceId`time`temperature, [SYMBOL,DATETIME,DOUBLE]), `tb, `deviceId`time)
n = 3000000
t.append!(table(rand(deviceId, n) as deviceId, 2019.06.01T00:00:00 + rand(86400 * 10, n) as time, 60 + norm(0.0, 5.0, n) as temperature))

// 重新分区
ds = repartitionDS(<select deviceId, temperature from t>, `deviceId)
// 执行计算
res = mr(ds, def(t) { return select percentile(temperature, 95) from t group by deviceId}, , unionAll{, false})

这个计算的结果正确性能够保证,因为repartitionDS产生的新分区基于deviceId的原有分区,能确保其中各个数据源的deviceId两两不重合,因此只需要将各分区计算结果合并就能取得正确结果。


3.3 textChunkDS函数

textChunkDS函数可以将一个文本文件分成若干个数据源,以便对一个文本文件所表示的数据执行分布式计算。它的语法是:textChunkDS(filename, chunkSize, [delimiter=','], [schema])。其中,filename, delimiter, schema这些参数与loadText函数的参数相同。而chunkSize参数表示每个数据源中数据的大小,单位为MB,可以取1到2047的整数。

以下例子是官方文档中olsEx例子的另一种实现。它通过textChunkDS函数从文本文件中生成若干数据源,每个数据源的大小为100MB,对生成的数据源经过转换后,执行olsEx函数,计算最小二乘参数:

ds = textChunkDS("c:/DolphinDB/Data/USPrices.csv", 100)
ds.transDS!(USPrices -> select VOL\SHROUT as VS, abs(RET) as ABS_RET, RET, log(SHROUT*(BID+ASK)\2) as SBA from USPrices where VOL>0)
rs=olsEx(ds, `VS, `ABS_RET`SBA, true, 2)

其中的数据源转换操作transDS!,可以参考3.6节。


3.4 第三方数据源提供的数据源接口

一些加载第三方数据的插件,例如HDF5,提供了产生数据源的接口。用户可以直接对它们返回的数据源执行分布式算法,而无需先将第三方数据导入内存或保存为磁盘或分布式表。

DolphinDB的HDF5插件提供了hdf5DS函数,用户可以通过设置其dsNum参数,指定需要生成的数据源个数。以下例子从HDF5文件中生成10个数据源,并通过Map-Reduce框架对结果的第1列求样本方差:

ds = hdf5::hdf5DS("large_file.h5", "large_table", , 10)

def varMap(t) {
    column = t.col(0)
    return [column.sum(), column.sum2(), column.count()]
}

def varFinal(result) {
    sum, sum2, count = result
    mu = sum \ count
    populationVar = sum2 \ count - mu * mu
    sampleVar = populationVar * count \ (count - 1)
    return sampleVar
}

sampleVar = mr(ds, varMap, +, varFinal)


3.5 数据源缓存

数据源可以有0,1或多个位置。位置为0的数据源是本地数据源。 在多个位置的情况下,这些位置互为备份。系统会随机选择一个位置执行分布式计算。当数据源被指示缓存数据对象时,系统会选择我们上次成功检索数据的位置。

用户可以指示系统对数据源进行缓存或清理缓存。对于迭代计算算法(例如机器学习算法),数据缓存可以大大提高计算性能。当系统内存不足时,缓存数据将被清除。如果发生这种情况,系统可以恢复数据,因为数据源包含所有元描述和数据转换函数。

和数据源缓存相关的函数有:

  • cacheDS!:指示系统缓存数据源
  • clearcacheDS!:指示系统在下次执行数据源之后清除缓存
  • cacheDSNow:立即执行并缓存数据源,并返回缓存行的总数
  • clearCacheDSNow:立即清除数据源和缓存


3.6 数据源转换

一个数据源对象还可以包含多个数据转换函数,用以进一步处理所检索到的数据。系统会依次执行这些数据转换函数,一个函数的输出作为下一个函数的输入(和唯一的输入)。

将数据转换函数包含在数据源中,通常比在核心计算操作(即map函数)中对数据源进行转换更有效。如果检索到的数据仅需要一次计算时,没有性能差异,但它对于具有缓存数据对象的数据源的迭代计算会造成巨大的差异。如果转换操作在核心计算操作中,则每次迭代都需要执行转换; 如果转换操作在数据源中,则它们只被执行一次。transDS!函数提供了转换数据源的功能。

例如,执行迭代机器学习函数randomForestRegressor之前,用户可能需要手动填充数据的缺失值(当然,DolphinDB的随机森林算法已经内置了缺失值处理)。此时,可以用transDS!对数据源进行如下处理:对每一个特征列,用该列的平均值填充缺失值。假设表中的列x0, x1, x2, x3为自变量,列y为因变量,以下是实现方法:

ds = sqlDS(<select x0, x1, x2, x3, y from t>)
ds.transDS!(def (mutable t) {
    update t set x0 = nullFill(x0, avg(x0)), x1 = nullFill(x1, avg(x1)), x2 = nullFill(x2, avg(x2)), x3 = nullFill(x3, avg(x3))
    return t
})

randomForestRegressor(ds, `y, `x0`x1`x2`x3)

另一个转换数据源的例子是2.2节提到的逻辑回归的脚本实现。在2.2节的实现中,map函数调用中包含了从数据源的表中取出对应列,转换成矩阵的操作,这意味着每一轮迭代都会发生这些操作。而实际上,每轮迭代都会使用同样的输入矩阵,这个转换步骤只需要调用一次。因此,可以用transDS!将数据源转换成一个包含x, xt和y矩阵的三元组:

def myLrTrans(t, yColName, xColNames, intercept) {
    if (intercept)
        x = matrix(t[xColNames], take(1.0, t.rows()))
    else
        x = matrix(t[xColNames])
    xt = x.transpose()
    y = t[yColName]
    return [x, xt, y]
}

def myLrMap(input, lastFinal) {
    x, xt, y = input
    placeholder, placeholder, theta = lastFinal
    // 之后的计算和2.2节相同
}

// myLrFinal和myLrTerm函数和2.2节相同

def myLr(mutable ds, yColName, xColNames, intercept, initTheta, tol) {
    ds.transDS!(myLrTrans{, yColName, xColNames, intercept})
    logLik, grad, theta = imr(ds, [0, 0, initTheta], myLrMap, +, myLrFinal, myLrTerm{, , tol})
    return theta
}


おすすめ

転載: blog.51cto.com/15022783/2677988