Produits secs 丨 Tutoriel informatique général DolphinDB

DolphinDB peut non seulement stocker des données distribuées, mais a également un bon support pour l'informatique distribuée. Dans DolphinDB, les utilisateurs peuvent utiliser le cadre général de calcul distribué fourni par le système pour implémenter des algorithmes distribués efficaces via des scripts sans prêter attention aux implémentations sous-jacentes spécifiques. Cet article donnera une explication détaillée des concepts importants et des fonctions associées dans le cadre de calcul général de DolphinDB, et fournira une multitude de scénarios et d'exemples d'utilisation spécifiques.


1. Source de données

La source de données est le concept de base du cadre informatique général de DolphinDB. Il s'agit d'un type spécial d'objet de données, qui est une méta description des données. En exécutant la source de données, les utilisateurs peuvent obtenir des entités de données telles que des tables, des matrices et des vecteurs. Dans le cadre de calcul distribué de DolphinDB, des objets de source de données légers au lieu d'énormes entités de données sont transmis aux nœuds distants pour des calculs ultérieurs, ce qui réduit considérablement le trafic réseau.

Dans DolphinDB, les utilisateurs utilisent souvent la fonction sqlDS pour générer une source de données basée sur une expression SQL. Cette fonction n'interroge pas directement la table, mais renvoie la méta-instruction d'une ou plusieurs sous-requêtes SQL, c'est-à-dire la source de données. Ensuite, les utilisateurs peuvent utiliser le framework Map-Reduce pour transmettre des sources de données et des fonctions de calcul, distribuer des tâches aux nœuds correspondant à chaque source de données, effectuer les calculs en parallèle, puis résumer les résultats.

Plusieurs méthodes couramment utilisées pour obtenir des sources de données seront décrites en détail dans les sections 3.1, 3.2, 3.3 et 3.4 de cet article.


2. Cadre Map-Reduce

La fonction Map-Reduce est la fonction principale du cadre général de calcul distribué DolphinDB.

Fonction 2.1 mr

La syntaxe de la fonction Map-Reduce mr de DolphinDB est mr (ds, mapFunc, [reductionFunc], [finalFunc], [parallel = true]), qui accepte un ensemble de sources de données et une fonction mapFunc comme paramètres. Il distribue les tâches de calcul aux nœuds où se trouve chaque source de données et traite les données dans chaque source de données via mapFunc. Le paramètre facultatif reductionFunc calculera la valeur de retour de mapFunc par paires, et le résultat obtenu sera calculé avec la valeur de retour du troisième mapFunc, de sorte que le calcul cumulatif résumera les résultats de mapFunc. S'il y a des appels de carte M, la fonction de réduction sera appelée M-1 fois. Le paramètre facultatif finalFunc traite en outre la valeur de retour de reductionFunc.

Il y a un exemple d'exécution de la régression linéaire des moindres carrés distribués via mr dans le document officiel . Cet article utilise l'exemple suivant pour montrer comment utiliser un appel mr pour échantillonner au hasard un dixième des données dans chaque partition de la table distribuée:

// Créer une base de données et une table 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)) 

// définir la carte Fonction 
def sampleMap (t) { 
    sampleRate = 0,1 
    rowNum = t.rows () 
    sampleIndex = (0 .. (rowNum-1)). Shuffle () [0: int (rowNum * sampleRate)] 
    return t [sampleIndex] 
} 

ds = sqlDS (<select * from t>) // Créer une source de données 
res = mr (ds, sampleMap ,, unionAll) // Effectuer le calcul

Dans l'exemple ci-dessus, la fonction sampleMap définie par l'utilisateur accepte une table (c'est-à-dire les données de la source de données) en tant que paramètre et renvoie de manière aléatoire un dixième des lignes. La fonction mr de cet exemple n'a pas de paramètre ReduceFunc, de sorte que la valeur de retour de chaque fonction de mappage est placée dans un tuple et transmise à finalFunc, c'est-à-dire unionAll. unionAll fusionne les multiples tables renvoyées par la fonction map dans une table distribuée avec des partitions séquentielles.

2.2 Fonction imr

La base de données DolphinDB fournit une fonction de calcul itérative imr basée sur la méthode Map-Reduce. Par rapport à mr, il peut prendre en charge le calcul itératif. Chaque itération utilise le résultat de l'itération précédente et l'ensemble de données d'entrée, ce qui lui permet de prendre en charge la mise en œuvre d'algorithmes plus complexes. Le calcul itératif nécessite les valeurs initiales et les critères de terminaison des paramètres du modèle. Sa syntaxe est imr (ds, initValue, mapFunc, [reductionFunc], [finalFunc], terminateFunc, [carryover = false]) où le paramètre initValue est la valeur initiale de la première itération, le paramètre mapFunc est une fonction et le paramètre accepté les paramètres incluent L'entité de source de données et la sortie de la fonction finale dans l'itération précédente. Pour la première itération, c'est la valeur initiale donnée par l'utilisateur. Les paramètres dans imr sont similaires à la fonction mr. La fonction finalFunc accepte deux paramètres, le premier paramètre est la sortie de la fonction finale dans l'itération précédente. Pour la première itération, c'est la valeur initiale donnée par l'utilisateur. Le deuxième paramètre est la sortie après l'appel de la fonction de réduction. Le paramètre terminateFunc est utilisé pour déterminer si l'itération est terminée. Il accepte deux paramètres. Le premier est la sortie de la fonction de réduction dans l'itération précédente, et le second est la sortie de la fonction de réduction dans l'itération actuelle. S'il renvoie true, l'itération sera abandonnée. Le paramètre de report indique si l'appel de fonction de carte génère un objet à passer à l'appel de fonction de carte suivant. Si le report est vrai, alors la fonction de carte a 3 paramètres et le dernier paramètre est l'objet transporté, et le résultat de sortie de la fonction de carte est un tuple, et le dernier élément est l'objet transporté. Dans la première itération, l'objet transporté est NULL.

Il existe un exemple de calcul de la médiane des données distribuées via imr dans le document officiel . Cet article fournira un exemple plus complexe, c'est-à-dire en utilisant la méthode de Newton pour réaliser un calcul de régression logistique (régression logistique), pour montrer l'application de imr dans les algorithmes d'apprentissage automatique.

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
}


Je suppose que tu aimes

Origine blog.51cto.com/15022783/2677988
conseillé
Classement