Python对阵Julia:机器学习实例

译文
开发 后端 前端
在之前谈到的保序回归加速话题中,我们聊起如何利用Cython改进回归算法的性能表现。我觉得将Python优化代码的性能表现与原生Julia方案加以比对能够进一步明确大家对于速度提升的直观感受。

在之前谈到的保序回归加速话题中,我们聊起如何利用Cython改进回归算法的性能表现。我觉得将Python优化代码的性能表现与原生Julia方案加以比对能够进一步明确大家对于速度提升的直观感受。

今天的文章将承接上一篇,因此大家在进行阅读前,不妨先对前文进行一番回顾、旨在掌握相关背景信息。

我们将借用前文中提到的两种算法,并在这里就性能表现在Julia与Python之间展开一番比拼。

线性PAVA

相关Cython代码可以通过Github上的scikit-learn进行下载,而Julia代码则来自GitHub上的Isotonic.jl。

Julia代码采用的是最为简单的PAVA表达,不掺杂任何花哨的内容与修饰;@inbounds宏的作用是客观比较Cython的执行效果并关闭bound check。

  1. function isotonic_regression(y::Vector{Float64}, weights::Vector{Float64})  
  2.     @inbounds begin  
  3.         n = size(y, 1)  
  4.         if n <= 1 
  5.             return y  
  6.         end  
  7.         n -= 1 
  8.         while true  
  9.             i = 1 
  10.             pooled = 0 
  11.             while i <= n  
  12.                 k = i  
  13.                 while k <= n && y[k] >= y[k+1]  
  14.                     k += 1 
  15.                 end  
  16.    
  17.                 # Find a decreasing subsequence, and update  
  18.                 # all points in the sequence to the weighted average.  
  19.                 if y[i] != y[k]  
  20.                     numerator = 0.0 
  21.                     denominator = 0.0 
  22.                     for j in i : k  
  23.                         numerator += y[j] * weights[j]  
  24.                         denominator += weights[j]  
  25.                     end  
  26.    
  27.                     for j in i : k  
  28.                         y[j] = numerator / denominator  
  29.                     end  
  30.                     pooled = 1 
  31.                 end  
  32.                 i = k + 1 
  33.             end  
  34.             if pooled == 0 
  35.                 break 
  36.             end  
  37.         end  
  38.     end  
  39.     return y  
  40. end  
  41.    
  42. isotonic_regression(y::Vector{Float64}) = isotonic_regression(y, ones(size(y, 1)))  

linear_pava.jl hosted with ❤ by GitHub

  1. @cython.boundscheck(False)  
  2. @cython.wraparound(False)  
  3. @cython.cdivision(True)  
  4. def _isotonic_regression(np.ndarray[DOUBLE, ndim=1] y,  
  5.                          np.ndarray[DOUBLE, ndim=1] weight,  
  6.                          np.ndarray[DOUBLE, ndim=1] solution):  
  7.     cdef:  
  8.         DOUBLE numerator, denominator  
  9.         Py_ssize_t i, pooled, n, k  
  10.    
  11.     n = y.shape[0]  
  12.     # The algorithm proceeds by iteratively updating the solution  
  13.     # array.  
  14.    
  15.     # TODO - should we just pass in a pre-copied solution  
  16.     # array and mutate that?  
  17.     for i in range(n):  
  18.         solution[i] = y[i]  
  19.    
  20.     if n <= 1:  
  21.         return solution  
  22.    
  23.     n -1 
  24.     while 1:  
  25.         # repeat until there are no more adjacent violators.  
  26.         i = 0 
  27.         pooled = 0 
  28.         while i < n: 
  29.             k = i 
  30.             while k < n and solution[k] >= solution[k + 1]:  
  31.                 k += 1  
  32.             if solution[i] != solution[k]:  
  33.                 # solution[i:k + 1] is a decreasing subsequence, so  
  34.                 # replace each point in the subsequence with the  
  35.                 # weighted average of the subsequence.  
  36.    
  37.                 # TODO: explore replacing each subsequence with a  
  38.                 # _single_ weighted point, and reconstruct the whole  
  39.                 # sequence from the sequence of collapsed points.  
  40.                 # Theoretically should reduce running time, though  
  41.                 # initial experiments weren't promising.  
  42.                 numerator = 0.0  
  43.                 denominator = 0.0  
  44.                 for j in range(i, k + 1):  
  45.                     numerator += solution[j] * weight[j]  
  46.                     denominator += weight[j]  
  47.                 for j in range(i, k + 1):  
  48.                     solution[j] = numerator / denominator  
  49.                 pooled = 1 
  50.             i = k + 1  
  51.         # Check for convergence  
  52.         if pooled == 0:  
  53.             break  
  54.    
  55.     return solution  

_isotonic.pyx hosted with ❤ by GitHub

Active Set

Active Set的行数与Cython代码基本相当,而且在结构上可能更为简洁(通过显式复合type ActiveState实现)、旨在维持给定主动双重变量中的参数。Active Set会将重复代码拆分为独立函数,从而由LLVM对其实现内联——这一点很难借由Cython中的任何参数实现。

Julia中的检索机制也会对算法作出一定程度的精简。

  1. immutable ActiveState  
  2.     weighted_label::Float64  
  3.     weight::Float64  
  4.     lower::Int64  
  5.     upper::Int64  
  6.    
  7. end  
  8.    
  9. function merge_state(l::ActiveState, r::ActiveState)  
  10.     return ActiveState(l.weighted_label + r.weighted_label,  
  11.                        l.weight + r.weight,  
  12.                        l.lower,  
  13.                        r.upper)  
  14. end  
  15.    
  16. function below(l::ActiveState, r::ActiveState)  
  17.     return l.weighted_label * r.weight <= l.weight * r.weighted_label  
  18. end  
  19.    
  20. function active_set_isotonic_regression(y::Vector{Float64}, weights::Vector{Float64})  
  21.     @inbounds begin  
  22.         active_set = [ActiveState(weights[i] * y[i], weights[i], i, i) for i in 1 : size(y, 1)]  
  23.         current = 1 
  24.         while current < size(active_set, 1)  
  25.             while current < size(active_set, 1) && below(active_set[current], active_set[current+1])  
  26.                 current += 1 
  27.             end  
  28.             if current == size(active_set, 1)  
  29.                 break 
  30.             end  
  31.    
  32.             merged = merge_state(active_set[current], active_set[current+1])  
  33.             splice!(active_set, current:current+1, [merged])  
  34.             while current > 1 && !below(active_set[current-1], active_set[current])  
  35.                 current -= 1 
  36.                 merged = merge_state(active_set[current], active_set[current+1])  
  37.                 splice!(active_set, current:current+1, [merged])  
  38.             end  
  39.         end  
  40.    
  41.         for as in active_set  
  42.             y[as.lower:as.upper] = as.weighted_label / as.weight  
  43.         end  
  44.     end  
  45.     return y  
  46. end  
  47.    
  48. active_set_isotonic_regression(y::Vector{Float64}) = active_set_isotonic_regression(y, ones(size(y, 1)))  

active_set.jl hosted with ❤ by GitHub

  1. @cython.boundscheck(False)  
  2. @cython.wraparound(False)  
  3. @cython.cdivision(True)  
  4. def _isotonic_regression(np.ndarray[DOUBLE, ndim=1] y,  
  5.                          np.ndarray[DOUBLE, ndim=1] weight,  
  6.                          np.ndarray[DOUBLE, ndim=1] solution):  
  7.    
  8.     cdef:  
  9.         Py_ssize_t current, i  
  10.         unsigned int len_active_set  
  11.         DOUBLE v, w  
  12.    
  13.     len_active_set = y.shape[0]  
  14.     active_set = [[weight[i] * y[i], weight[i], [i, ]]  
  15.                   for i in range(len_active_set)]  
  16.     current = 0 
  17.    
  18.     while current < len_active_set - 1:  
  19.         while current < len_active_set -1 and \  
  20.               (active_set[current][0] * active_set[current + 1][1] <=   
  21.                active_set[current][1] * active_set[current + 1][0]):  
  22.             current += 1 
  23.    
  24.         if current == len_active_set - 1:  
  25.             break 
  26.    
  27.         # merge two groups  
  28.         active_set[current][0] += active_set[current + 1][0]  
  29.         active_set[current][1] += active_set[current + 1][1]  
  30.         active_set[current][2] += active_set[current + 1][2]  
  31.    
  32.         active_set.pop(current + 1)  
  33.         len_active_set -= 1 
  34.         while current > 0 and \  
  35.               (active_set[current - 1][0] * active_set[current][1] >   
  36.                active_set[current - 1][1] * active_set[current][0]):  
  37.             current -= 1 
  38.             active_set[current][0] += active_set[current + 1][0]  
  39.             active_set[current][1] += active_set[current + 1][1]  
  40.             active_set[current][2] += active_set[current + 1][2]  
  41.    
  42.             active_set.pop(current + 1)  
  43.             len_active_set -= 1 
  44.    
  45.     for v, w, idx in active_set:  
  46.         solution[idx] = v / w  
  47.     return solution  

_isotonic.pyx hosted with ❤ by GitHub 

性能表现

可以看到,在与Cython实例进行比对时,我们发现即使是同样的算法在Julia中的平均执行速度也会更快。


[点击扩大]

对于上述Active Set实例,Julia在处理回归计算时的表现都能实现5倍到300倍的速度提升。

对于线性PAVA实例,Julia的速度提升效果则为1.1倍到4倍之间。

这样的结果证明,Julia在性能敏感型机器学习应用领域具有显著吸引力。

大家可以点击此处了解更多关于上述性能测试结果的获取方法。

英文原文:http://tullo.ch/articles/python-vs-julia/

责任编辑:林师授 来源: 51CTO
相关推荐

2019-12-16 14:53:44

机器学习人工智能计算机

2018-12-12 09:33:58

编程语言机器学习代码

2022-01-13 15:55:20

开发技能代码

2016-08-31 06:55:45

机器学习标题诱饵

2020-04-17 14:35:28

JuliaPython编程

2020-05-06 09:15:40

Python Julia编程语言

2020-09-10 11:20:37

Python机器学习人工智能

2020-12-16 15:56:26

机器学习人工智能Python

2020-05-17 14:37:37

机器学习技术架构

2018-09-13 08:19:50

Python Java 编程语言

2020-09-22 15:16:49

Python编程语言Julia

2024-02-05 09:30:10

推荐算法深度学习内容过滤

2020-10-18 21:33:35

PythonJuliaSwift

2020-05-25 09:06:58

Julia语言Python

2019-08-13 10:53:04

2022-03-11 08:00:00

编程语言JuliaPython

2021-07-29 13:06:29

Python机器学习编程语言

2022-06-05 21:16:08

机器学习Python

2022-06-09 09:14:31

机器学习PythonJava

2016-11-03 09:19:04

Python机器学习库
点赞
收藏

51CTO技术栈公众号