在 Theano 中循环(或向量化)可变长度矩阵
Loop over (or vectorize) variable length matrices in Theano
我有一个矩阵列表 L
,其中每个项目 M
是一个 x*n
矩阵(x
是一个变量,n
是一个持续的)。
我想为 L
中的所有项目计算 M'*M
的总和(M'
是 M
的转置)如下 Python代码:
for M in L:
res += np.dot(M.T, M)
实际上我想在 Theano 中实现这个(它不支持可变长度的多维数组),我不想将所有矩阵填充到相同的大小,因为那样会浪费太多 space (有些矩阵可能非常大)。
有更好的方法吗?
编辑:
L
在 Theano 编译之前就知道了。
编辑:
收到来自@DanielRenshaw 和@Divakar 的两个优秀答案,情感上难以选择一个接受。
鉴于在需要进行 Theano 编译之前已知矩阵的数量,可以简单地使用常规的 Python Theano 矩阵列表。
这是一个完整的示例,显示了 numpy 和 Theano 版本之间的区别。
此代码已更新,包括与@Divakar 的矢量化方法的比较,后者性能更好。 Theano 有两种向量化方法,一种是 Theano 执行连接,另一种是 numpy 执行连接,然后将结果传递给 Theano。
import timeit
import numpy as np
import theano
import theano.tensor as tt
def compile_theano_version1(number_of_matrices, n, dtype):
assert number_of_matrices > 0
assert n > 0
L = [tt.matrix() for _ in xrange(number_of_matrices)]
res = tt.zeros(n, dtype=dtype)
for M in L:
res += tt.dot(M.T, M)
return theano.function(L, res)
def compile_theano_version2(number_of_matrices):
assert number_of_matrices > 0
L = [tt.matrix() for _ in xrange(number_of_matrices)]
concatenated_L = tt.concatenate(L, axis=0)
res = tt.dot(concatenated_L.T, concatenated_L)
return theano.function(L, res)
def compile_theano_version3():
concatenated_L = tt.matrix()
res = tt.dot(concatenated_L.T, concatenated_L)
return theano.function([concatenated_L], res)
def numpy_version1(*L):
assert len(L) > 0
n = L[0].shape[1]
res = np.zeros((n, n), dtype=L[0].dtype)
for M in L:
res += np.dot(M.T, M)
return res
def numpy_version2(*L):
concatenated_L = np.concatenate(L, axis=0)
return np.dot(concatenated_L.T, concatenated_L)
def main():
iteration_count = 100
number_of_matrices = 20
n = 300
min_x = 400
dtype = 'float64'
theano_version1 = compile_theano_version1(number_of_matrices, n, dtype)
theano_version2 = compile_theano_version2(number_of_matrices)
theano_version3 = compile_theano_version3()
L = [np.random.standard_normal(size=(x, n)).astype(dtype)
for x in range(min_x, number_of_matrices + min_x)]
start = timeit.default_timer()
numpy_res1 = np.sum(numpy_version1(*L)
for _ in xrange(iteration_count))
print 'numpy_version1', timeit.default_timer() - start
start = timeit.default_timer()
numpy_res2 = np.sum(numpy_version2(*L)
for _ in xrange(iteration_count))
print 'numpy_version2', timeit.default_timer() - start
start = timeit.default_timer()
theano_res1 = np.sum(theano_version1(*L)
for _ in xrange(iteration_count))
print 'theano_version1', timeit.default_timer() - start
start = timeit.default_timer()
theano_res2 = np.sum(theano_version2(*L)
for _ in xrange(iteration_count))
print 'theano_version2', timeit.default_timer() - start
start = timeit.default_timer()
theano_res3 = np.sum(theano_version3(np.concatenate(L, axis=0))
for _ in xrange(iteration_count))
print 'theano_version3', timeit.default_timer() - start
assert np.allclose(numpy_res1, numpy_res2)
assert np.allclose(numpy_res2, theano_res1)
assert np.allclose(theano_res1, theano_res2)
assert np.allclose(theano_res2, theano_res3)
main()
当 运行 打印时(类似于)
numpy_version1 1.47830819649
numpy_version2 1.77405482179
theano_version1 1.3603150303
theano_version2 1.81665318145
theano_version3 1.86912039489
断言通过,表明 Theano 和 numpy 版本都以高精度计算相同的结果。显然,如果使用 float32
而不是 float64
.
,这种准确性会降低
时序结果表明向量化方法可能并不可取,这取决于矩阵大小。在上面的示例中,矩阵很大,non-concatenation 方法更快,但是如果 n
和 min_x
参数在 main
函数中更改为更小,那么串联方法更快。 运行在 GPU 上运行时(仅限 Theano 版本)可能会出现其他结果。
您可以只沿着第一个轴填充输入数组,即所有 x
的总和。因此,我们最终会得到一个高 (X,n)
数组,其中 X =x1+x2+x3+....
。这可以被转置,它与自身的点积将是形状 (n,n)
的所需输出。所有这一切都是通过利用强大的点积的纯矢量化解决方案实现的。因此,实现将是 -
# Concatenate along axis=0
Lcat = np.concatenate(L,axis=0)
# Perform dot product of the transposed version with self
out = Lcat.T.dot(Lcat)
运行时测试并验证输出 -
In [116]: def vectoized_approach(L):
...: Lcat = np.concatenate(L,axis=0)
...: return Lcat.T.dot(Lcat)
...:
...: def original_app(L):
...: n = L[0].shape[1]
...: res = np.zeros((n,n))
...: for M in L:
...: res += np.dot(M.T, M)
...: return res
...:
In [117]: # Input
...: L = [np.random.rand(np.random.randint(1,9),5)for iter in range(1000)]
In [118]: np.allclose(vectoized_approach(L),original_app(L))
Out[118]: True
In [119]: %timeit original_app(L)
100 loops, best of 3: 3.84 ms per loop
In [120]: %timeit vectoized_approach(L)
1000 loops, best of 3: 632 µs per loop
除了@DanielRenshaw 的回答,如果我们将矩阵的数量增加到 1000,compile_theano_version1
函数将产生 RuntimeError: maximum recursion depth exceeded
,而 compile_theano_version2
似乎需要很长时间才能编译。
使用 typed_list
:
可以解决这个问题
def compile_theano_version4(number_of_matrices, n):
import theano.typed_list
L = theano.typed_list.TypedListType(tt.TensorType(theano.config.floatX, broadcastable=(None, None)))()
res, _ = theano.scan(fn=lambda i: tt.dot(L[i].T, L[i]),
sequences=[theano.tensor.arange(number_of_matrices, dtype='int64')])
return theano.function([L], res.sum(axis=0))
此外,我将所有相关变量的数据类型设置为float32
和运行@DanielRenshaw在GPU上的脚本,结果是@Divakar的建议(theano_version3
)是在这种情况下效率最高。尽管正如@DanielRenshaw 所说,使用巨大的矩阵可能并不总是一个好习惯。
以下是我机器上的设置和输出。
iteration_count = 100
number_of_matrices = 200
n = 300
min_x = 20
dtype = 'float32'
theano.config.floatX = dtype
numpy_version1 5.30542397499
numpy_version2 3.96656394005
theano_version1 5.26742005348
theano_version2 1.76983904839
theano_version3 1.03577589989
theano_version4 5.58366179466
我有一个矩阵列表 L
,其中每个项目 M
是一个 x*n
矩阵(x
是一个变量,n
是一个持续的)。
我想为 L
中的所有项目计算 M'*M
的总和(M'
是 M
的转置)如下 Python代码:
for M in L:
res += np.dot(M.T, M)
实际上我想在 Theano 中实现这个(它不支持可变长度的多维数组),我不想将所有矩阵填充到相同的大小,因为那样会浪费太多 space (有些矩阵可能非常大)。
有更好的方法吗?
编辑:
L
在 Theano 编译之前就知道了。
编辑:
收到来自@DanielRenshaw 和@Divakar 的两个优秀答案,情感上难以选择一个接受。
鉴于在需要进行 Theano 编译之前已知矩阵的数量,可以简单地使用常规的 Python Theano 矩阵列表。
这是一个完整的示例,显示了 numpy 和 Theano 版本之间的区别。
此代码已更新,包括与@Divakar 的矢量化方法的比较,后者性能更好。 Theano 有两种向量化方法,一种是 Theano 执行连接,另一种是 numpy 执行连接,然后将结果传递给 Theano。
import timeit
import numpy as np
import theano
import theano.tensor as tt
def compile_theano_version1(number_of_matrices, n, dtype):
assert number_of_matrices > 0
assert n > 0
L = [tt.matrix() for _ in xrange(number_of_matrices)]
res = tt.zeros(n, dtype=dtype)
for M in L:
res += tt.dot(M.T, M)
return theano.function(L, res)
def compile_theano_version2(number_of_matrices):
assert number_of_matrices > 0
L = [tt.matrix() for _ in xrange(number_of_matrices)]
concatenated_L = tt.concatenate(L, axis=0)
res = tt.dot(concatenated_L.T, concatenated_L)
return theano.function(L, res)
def compile_theano_version3():
concatenated_L = tt.matrix()
res = tt.dot(concatenated_L.T, concatenated_L)
return theano.function([concatenated_L], res)
def numpy_version1(*L):
assert len(L) > 0
n = L[0].shape[1]
res = np.zeros((n, n), dtype=L[0].dtype)
for M in L:
res += np.dot(M.T, M)
return res
def numpy_version2(*L):
concatenated_L = np.concatenate(L, axis=0)
return np.dot(concatenated_L.T, concatenated_L)
def main():
iteration_count = 100
number_of_matrices = 20
n = 300
min_x = 400
dtype = 'float64'
theano_version1 = compile_theano_version1(number_of_matrices, n, dtype)
theano_version2 = compile_theano_version2(number_of_matrices)
theano_version3 = compile_theano_version3()
L = [np.random.standard_normal(size=(x, n)).astype(dtype)
for x in range(min_x, number_of_matrices + min_x)]
start = timeit.default_timer()
numpy_res1 = np.sum(numpy_version1(*L)
for _ in xrange(iteration_count))
print 'numpy_version1', timeit.default_timer() - start
start = timeit.default_timer()
numpy_res2 = np.sum(numpy_version2(*L)
for _ in xrange(iteration_count))
print 'numpy_version2', timeit.default_timer() - start
start = timeit.default_timer()
theano_res1 = np.sum(theano_version1(*L)
for _ in xrange(iteration_count))
print 'theano_version1', timeit.default_timer() - start
start = timeit.default_timer()
theano_res2 = np.sum(theano_version2(*L)
for _ in xrange(iteration_count))
print 'theano_version2', timeit.default_timer() - start
start = timeit.default_timer()
theano_res3 = np.sum(theano_version3(np.concatenate(L, axis=0))
for _ in xrange(iteration_count))
print 'theano_version3', timeit.default_timer() - start
assert np.allclose(numpy_res1, numpy_res2)
assert np.allclose(numpy_res2, theano_res1)
assert np.allclose(theano_res1, theano_res2)
assert np.allclose(theano_res2, theano_res3)
main()
当 运行 打印时(类似于)
numpy_version1 1.47830819649
numpy_version2 1.77405482179
theano_version1 1.3603150303
theano_version2 1.81665318145
theano_version3 1.86912039489
断言通过,表明 Theano 和 numpy 版本都以高精度计算相同的结果。显然,如果使用 float32
而不是 float64
.
时序结果表明向量化方法可能并不可取,这取决于矩阵大小。在上面的示例中,矩阵很大,non-concatenation 方法更快,但是如果 n
和 min_x
参数在 main
函数中更改为更小,那么串联方法更快。 运行在 GPU 上运行时(仅限 Theano 版本)可能会出现其他结果。
您可以只沿着第一个轴填充输入数组,即所有 x
的总和。因此,我们最终会得到一个高 (X,n)
数组,其中 X =x1+x2+x3+....
。这可以被转置,它与自身的点积将是形状 (n,n)
的所需输出。所有这一切都是通过利用强大的点积的纯矢量化解决方案实现的。因此,实现将是 -
# Concatenate along axis=0
Lcat = np.concatenate(L,axis=0)
# Perform dot product of the transposed version with self
out = Lcat.T.dot(Lcat)
运行时测试并验证输出 -
In [116]: def vectoized_approach(L):
...: Lcat = np.concatenate(L,axis=0)
...: return Lcat.T.dot(Lcat)
...:
...: def original_app(L):
...: n = L[0].shape[1]
...: res = np.zeros((n,n))
...: for M in L:
...: res += np.dot(M.T, M)
...: return res
...:
In [117]: # Input
...: L = [np.random.rand(np.random.randint(1,9),5)for iter in range(1000)]
In [118]: np.allclose(vectoized_approach(L),original_app(L))
Out[118]: True
In [119]: %timeit original_app(L)
100 loops, best of 3: 3.84 ms per loop
In [120]: %timeit vectoized_approach(L)
1000 loops, best of 3: 632 µs per loop
除了@DanielRenshaw 的回答,如果我们将矩阵的数量增加到 1000,compile_theano_version1
函数将产生 RuntimeError: maximum recursion depth exceeded
,而 compile_theano_version2
似乎需要很长时间才能编译。
使用 typed_list
:
def compile_theano_version4(number_of_matrices, n):
import theano.typed_list
L = theano.typed_list.TypedListType(tt.TensorType(theano.config.floatX, broadcastable=(None, None)))()
res, _ = theano.scan(fn=lambda i: tt.dot(L[i].T, L[i]),
sequences=[theano.tensor.arange(number_of_matrices, dtype='int64')])
return theano.function([L], res.sum(axis=0))
此外,我将所有相关变量的数据类型设置为float32
和运行@DanielRenshaw在GPU上的脚本,结果是@Divakar的建议(theano_version3
)是在这种情况下效率最高。尽管正如@DanielRenshaw 所说,使用巨大的矩阵可能并不总是一个好习惯。
以下是我机器上的设置和输出。
iteration_count = 100
number_of_matrices = 200
n = 300
min_x = 20
dtype = 'float32'
theano.config.floatX = dtype
numpy_version1 5.30542397499
numpy_version2 3.96656394005
theano_version1 5.26742005348
theano_version2 1.76983904839
theano_version3 1.03577589989
theano_version4 5.58366179466