对于一个简单的神经网络,我想将一个函数应用于 gonumVecDense
的所有值。
Gonum 有一个用于密集矩阵的Apply
方法,但不适用于向量,所以我手动执行此操作:
func sigmoid(z float64) float64 {
return 1.0 / (1.0 + math.Exp(-z))
}
func vSigmoid(zs *mat.VecDense) {
for i := 0; i < zs.Len(); i++ {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
}
}
这似乎是并发执行的明显目标,所以我尝试了
var wg sync.WaitGroup
func sigmoid(z float64) float64 {
wg.Done()
return 1.0 / (1.0 + math.Exp(-z))
}
func vSigmoid(zs *mat.VecDense) {
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go zs.SetVec(i, sigmoid(zs.AtVec(i)))
}
wg.Wait()
}
这不起作用,也许并不意外,因为Sigmoid()
不会以wg.Done()
结尾,因为 return 语句(完成所有工作(在它之后。
我的问题是:如何使用并发性将函数应用于 gonum 向量的每个元素?
首先请注意,这种并发计算的尝试假定SetVec()
和AtVec()
方法对于不同索引的并发使用是安全的。如果不是这种情况,则尝试的解决方案本质上是不安全的,并可能导致数据争用和未定义的行为。
应该调用wg.Done()
来表示"工人"goroutine完成了它的工作。但只有当goroutine完成它的工作时。
在您的情况下,它不是(仅(在worker goroutine中运行的sigmoid()
函数,而是zs.SetVec()
。所以你应该在zs.SetVec()
回来的时候打电话给wg.Done()
,而不是更早。
一种方法是在SetVec()
方法的末尾添加一个wg.Done()
(它也可以在开始时是一个defer wg.Done()
(,但是引入这种依赖是不可行的(SetVec()
不应该知道任何等待组和goroutines,这将严重限制其可用性(。
在这种情况下,最简单和最干净的方法是启动一个匿名函数(函数文字(作为worker goroutine,您可以在其中调用zs.SetVec()
,并且在返回上述函数后可以调用wg.Defer()
。
像这样:
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go func() {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
wg.Done()
}()
}
wg.Wait()
但是仅此一项是行不通的,因为函数文字(闭包(是指同时修改的循环变量,因此函数文字应该使用自己的副本,例如:
for i := 0; i < zs.Len(); i++ {
wg.Add(1)
go func(i int) {
zs.SetVec(i, sigmoid(zs.AtVec(i)))
wg.Done()
}(i)
}
wg.Wait()
另请注意,goroutines(尽管可能是轻量级的(确实有开销。如果他们所做的工作"很小",那么开销可能会超过利用多个内核/线程的性能提升,并且总体而言,您可能无法通过并发执行此类小任务来获得性能(见鬼,您甚至可能比不使用goroutines做得更糟糕(。量。
此外,您正在使用 goroutines 来完成最少的工作,一旦 goroutines 完成"微小"工作,您就可以通过不"丢弃"它们来提高性能,但您可以"重用"它们。请参阅相关问题:这是 Go 中的惯用工作线程池吗?