分类 软件开发 下的文章

让我们使用 C99 和 C++11 完成常见的数据科学任务。

虽然 PythonR 之类的语言在数据科学中越来越受欢迎,但是 C 和 C++ 对于高效的数据科学来说是一个不错的选择。在本文中,我们将使用 C99C++11 编写一个程序,该程序使用 Anscombe 的四重奏数据集,下面将对其进行解释。

我在一篇涉及 Python 和 GNU Octave 的文章中写了我不断学习编程语言的动机,值得大家回顾。这里所有的程序都需要在命令行上运行,而不是在图形用户界面(GUI)上运行。完整的示例可在 polyglot\_fit 存储库中找到。

编程任务

你将在本系列中编写的程序:

  • CSV 文件中读取数据
  • 用直线插值数据(即 f(x)=m ⋅ x + q
  • 将结果绘制到图像文件

这是许多数据科学家遇到的普遍情况。示例数据是 Anscombe 的四重奏的第一组,如下表所示。这是一组人工构建的数据,当拟合直线时可以提供相同的结果,但是它们的曲线非常不同。数据文件是一个文本文件,其中的制表符用作列分隔符,前几行作为标题。该任务将仅使用第一组(即前两列)。

C 语言的方式

C 语言是通用编程语言,是当今使用最广泛的语言之一(依据 TIOBE 指数RedMonk 编程语言排名编程语言流行度指数GitHub Octoverse 状态 得来)。这是一种相当古老的语言(大约诞生在 1973 年),并且用它编写了许多成功的程序(例如 Linux 内核和 Git 仅是其中的两个例子)。它也是最接近计算机内部运行机制的语言之一,因为它直接用于操作内存。它是一种编译语言;因此,源代码必须由编译器转换为机器代码。它的标准库很小,功能也不多,因此人们开发了其它库来提供缺少的功能。

我最常在数字运算中使用该语言,主要是因为其性能。我觉得使用起来很繁琐,因为它需要很多样板代码,但是它在各种环境中都得到了很好的支持。C99 标准是最新版本,增加了一些漂亮的功能,并且得到了编译器的良好支持。

我将一路介绍 C 和 C++ 编程的必要背景,以便初学者和高级用户都可以继续学习。

安装

要使用 C99 进行开发,你需要一个编译器。我通常使用 Clang,不过 GCC 是另一个有效的开源编译器。对于线性拟合,我选择使用 GNU 科学库。对于绘图,我找不到任何明智的库,因此该程序依赖于外部程序:Gnuplot。该示例还使用动态数据结构来存储数据,该结构在伯克利软件分发版(BSD)中定义。

Fedora 中安装很容易:

sudo dnf install clang gnuplot gsl gsl-devel

代码注释

在 C99 中,注释)的格式是在行的开头放置 //,行的其它部分将被解释器丢弃。另外,/**/ 之间的任何内容也将被丢弃。

// 这是一个注释,会被解释器忽略
/* 这也被忽略 */

必要的库

库由两部分组成:

  • 头文件,其中包含函数说明
  • 包含函数定义的源文件

头文件包含在源文件中,而库文件的源文件则链接到可执行文件。因此,此示例所需的头文件是:

// 输入/输出功能
#include <stdio.h>
// 标准库
#include <stdlib.h>
// 字符串操作功能
#include <string.h>
// BSD 队列
#include <sys/queue.h>
// GSL 科学功能
#include <gsl/gsl_fit.h>
#include <gsl/gsl_statistics_double.h>

主函数

在 C 语言中,程序必须位于称为主函数 main() 的特殊函数内:

int main(void) {
    ...
}

这与上一教程中介绍的 Python 不同,后者将运行在源文件中找到的所有代码。

定义变量

在 C 语言中,变量必须在使用前声明,并且必须与类型关联。每当你要使用变量时,都必须决定要在其中存储哪种数据。你也可以指定是否打算将变量用作常量值,这不是必需的,但是编译器可以从此信息中受益。 以下来自存储库中的 fitting\_C99.c 程序

const char *input_file_name = "anscombe.csv";
const char *delimiter = "\t";
const unsigned int skip_header = 3;
const unsigned int column_x = 0;
const unsigned int column_y = 1;
const char *output_file_name = "fit_C99.csv";
const unsigned int N = 100;

C 语言中的数组不是动态的,从某种意义上说,数组的长度必须事先确定(即,在编译之前):

int data_array[1024];

由于你通常不知道文件中有多少个数据点,因此请使用单链列表。这是一个动态数据结构,可以无限增长。幸运的是,BSD 提供了链表。这是一个示例定义:

struct data_point {
    double x;
    double y;

    SLIST_ENTRY(data_point) entries;
};

SLIST_HEAD(data_list, data_point) head = SLIST_HEAD_INITIALIZER(head);
SLIST_INIT(&head);

该示例定义了一个由结构化值组成的 data_point 列表,该结构化值同时包含 x 值和 y 值。语法相当复杂,但是很直观,详细描述它就会太冗长了。

打印输出

要在终端上打印,可以使用 printf() 函数,其功能类似于 Octave 的 printf() 函数(在第一篇文章中介绍):

printf("#### Anscombe's first set with C99 ####\n");

printf() 函数不会在打印字符串的末尾自动添加换行符,因此你必须添加换行符。第一个参数是一个字符串,可以包含传递给函数的其他参数的格式信息,例如:

printf("Slope: %f\n", slope);

读取数据

现在来到了困难的部分……有一些用 C 语言解析 CSV 文件的库,但是似乎没有一个库足够稳定或流行到可以放入到 Fedora 软件包存储库中。我没有为本教程添加依赖项,而是决定自己编写此部分。同样,讨论这些细节太啰嗦了,所以我只会解释大致的思路。为了简洁起见,将忽略源代码中的某些行,但是你可以在存储库中找到完整的示例代码。

首先,打开输入文件:

FILE* input_file = fopen(input_file_name, "r");

然后逐行读取文件,直到出现错误或文件结束:

while (!ferror(input_file) && !feof(input_file)) {
    size_t buffer_size = 0;
    char *buffer = NULL;
   
    getline(&buffer, &buffer_size, input_file);

    ...
}

getline() 函数是 POSIX.1-2008 标准新增的一个不错的函数。它可以读取文件中的整行,并负责分配必要的内存。然后使用 strtok() 函数将每一行分成 字元 token 。遍历字元,选择所需的列:

char *token = strtok(buffer, delimiter);

while (token != NULL)
{
    double value;
    sscanf(token, "%lf", &value);

    if (column == column_x) {
        x = value;
    } else if (column == column_y) {
        y = value;
    }

    column += 1;
    token = strtok(NULL, delimiter);
}

最后,当选择了 xy 值时,将新数据点插入链表中:

struct data_point *datum = malloc(sizeof(struct data_point));
datum->x = x;
datum->y = y;

SLIST_INSERT_HEAD(&head, datum, entries);

malloc() 函数为新数据点动态分配(保留)一些持久性内存。

拟合数据

GSL 线性拟合函数 gslfitlinear() 期望其输入为简单数组。因此,由于你将不知道要创建的数组的大小,因此必须手动分配它们的内存:

const size_t entries_number = row - skip_header - 1;

double *x = malloc(sizeof(double) * entries_number);
double *y = malloc(sizeof(double) * entries_number);

然后,遍历链表以将相关数据保存到数组:

SLIST_FOREACH(datum, &head, entries) {
    const double current_x = datum->x;
    const double current_y = datum->y;

    x[i] = current_x;
    y[i] = current_y;

    i += 1;
}

现在你已经处理完了链表,请清理它。要总是释放已手动分配的内存,以防止内存泄漏。内存泄漏是糟糕的、糟糕的、糟糕的(重要的话说三遍)。每次内存没有释放时,花园侏儒都会找不到自己的头:

while (!SLIST_EMPTY(&head)) {
    struct data_point *datum = SLIST_FIRST(&head);

    SLIST_REMOVE_HEAD(&head, entries);

    free(datum);
}

终于,终于!你可以拟合你的数据了:

gsl_fit_linear(x, 1, y, 1, entries_number,
               &intercept, &slope,
               &cov00, &cov01, &cov11, &chi_squared);
const double r_value = gsl_stats_correlation(x, 1, y, 1, entries_number);

printf("Slope: %f\n", slope);
printf("Intercept: %f\n", intercept);
printf("Correlation coefficient: %f\n", r_value);

绘图

你必须使用外部程序进行绘图。因此,将拟合数据保存到外部文件:

const double step_x = ((max_x + 1) - (min_x - 1)) / N;

for (unsigned int i = 0; i < N; i += 1) {
    const double current_x = (min_x - 1) + step_x * i;
    const double current_y = intercept + slope * current_x;

    fprintf(output_file, "%f\t%f\n", current_x, current_y);
}

用于绘制两个文件的 Gnuplot 命令是:

plot 'fit_C99.csv' using 1:2 with lines title 'Fit', 'anscombe.csv' using 1:2 with points pointtype 7 title 'Data'

结果

在运行程序之前,你必须编译它:

clang -std=c99 -I/usr/include/ fitting_C99.c -L/usr/lib/ -L/usr/lib64/ -lgsl -lgslcblas -o fitting_C99

这个命令告诉编译器使用 C99 标准、读取 fitting_C99.c 文件、加载 gslgslcblas 库、并将结果保存到 fitting_C99。命令行上的结果输出为:

#### Anscombe's first set with C99 ####
Slope: 0.500091
Intercept: 3.000091
Correlation coefficient: 0.816421

这是用 Gnuplot 生成的结果图像:

 title=

C++11 方式

C++ 语言是一种通用编程语言,也是当今使用的最受欢迎的语言之一。它是作为 C 的继承人创建的(诞生于 1983 年),重点是面向对象程序设计(OOP)。C++ 通常被视为 C 的超集,因此 C 程序应该能够使用 C++ 编译器进行编译。这并非完全正确,因为在某些极端情况下它们的行为有所不同。 根据我的经验,C++ 与 C 相比需要更少的样板代码,但是如果要进行面向对象开发,语法会更困难。C++11 标准是最新版本,增加了一些漂亮的功能,并且基本上得到了编译器的支持。

由于 C++ 在很大程度上与 C 兼容,因此我将仅强调两者之间的区别。我在本部分中没有涵盖的任何部分,则意味着它与 C 中的相同。

安装

这个 C++ 示例的依赖项与 C 示例相同。 在 Fedora 上,运行:

sudo dnf install clang gnuplot gsl gsl-devel

必要的库

库的工作方式与 C 语言相同,但是 include 指令略有不同:

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>

extern "C" {
#include <gsl/gsl_fit.h>
#include <gsl/gsl_statistics_double.h>
}

由于 GSL 库是用 C 编写的,因此你必须将这个特殊情况告知编译器。

定义变量

与 C 语言相比,C++ 支持更多的数据类型(类),例如,与其 C 语言版本相比,string 类型具有更多的功能。相应地更新变量的定义:

const std::string input_file_name("anscombe.csv");

对于字符串之类的结构化对象,你可以定义变量而无需使用 = 符号。

打印输出

你可以使用 printf() 函数,但是 cout 对象更惯用。使用运算符 << 来指示要使用 cout 打印的字符串(或对象):

std::cout << "#### Anscombe's first set with C++11 ####" << std::endl;

...

std::cout << "Slope: " << slope << std::endl;
std::cout << "Intercept: " << intercept << std::endl;
std::cout << "Correlation coefficient: " << r_value << std::endl;

读取数据

该方案与以前相同。将打开文件并逐行读取文件,但语法不同:

std::ifstream input_file(input_file_name);

while (input_file.good()) {
    std::string line;

    getline(input_file, line);

    ...
}

使用与 C99 示例相同的功能提取行字元。代替使用标准的 C 数组,而是使用两个向量。向量是 C++ 标准库中对 C 数组的扩展,它允许动态管理内存而无需显式调用 malloc()

std::vector<double> x;
std::vector<double> y;

// Adding an element to x and y:
x.emplace_back(value);
y.emplace_back(value);

拟合数据

要在 C++ 中拟合,你不必遍历列表,因为向量可以保证具有连续的内存。你可以将向量缓冲区的指针直接传递给拟合函数:

gsl_fit_linear(x.data(), 1, y.data(), 1, entries_number,
               &intercept, &slope,
               &cov00, &cov01, &cov11, &chi_squared);
const double r_value = gsl_stats_correlation(x.data(), 1, y.data(), 1, entries_number);

std::cout << "Slope: " << slope << std::endl;
std::cout << "Intercept: " << intercept << std::endl;
std::cout << "Correlation coefficient: " << r_value << std::endl;

绘图

使用与以前相同的方法进行绘图。 写入文件:

const double step_x = ((max_x + 1) - (min_x - 1)) / N;

for (unsigned int i = 0; i < N; i += 1) {
    const double current_x = (min_x - 1) + step_x * i;
    const double current_y = intercept + slope * current_x;

    output_file << current_x << "\t" << current_y << std::endl;
}

output_file.close();

然后使用 Gnuplot 进行绘图。

结果

在运行程序之前,必须使用类似的命令对其进行编译:

clang++ -std=c++11 -I/usr/include/ fitting_Cpp11.cpp -L/usr/lib/ -L/usr/lib64/ -lgsl -lgslcblas -o fitting_Cpp11

命令行上的结果输出为:

#### Anscombe's first set with C++11 ####
Slope: 0.500091
Intercept: 3.00009
Correlation coefficient: 0.816421

这就是用 Gnuplot 生成的结果图像:

 title=

结论

本文提供了用 C99 和 C++11 编写的数据拟合和绘图任务的示例。由于 C++ 在很大程度上与 C 兼容,因此本文利用了它们的相似性来编写了第二个示例。在某些方面,C++ 更易于使用,因为它部分减轻了显式管理内存的负担。但是其语法更加复杂,因为它引入了为 OOP 编写类的可能性。但是,仍然可以用 C 使用 OOP 方法编写软件。由于 OOP 是一种编程风格,因此可以在任何语言中使用。在 C 中有一些很好的 OOP 示例,例如 GObjectJansson库。

对于数字运算,我更喜欢在 C99 中进行,因为它的语法更简单并且得到了广泛的支持。直到最近,C++11 还没有得到广泛的支持,我倾向于避免使用先前版本中的粗糙不足之处。对于更复杂的软件,C++ 可能是一个不错的选择。

你是否也将 C 或 C++ 用于数据科学?在评论中分享你的经验。


via: https://opensource.com/article/20/2/c-data-science

作者:Cristiano L. Fontana 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解如何使用 Python 和 GNU Octave 完成一项常见的数据科学任务。

数据科学是跨越编程语言的知识领域。有些语言以解决这一领域的问题而闻名,而另一些则鲜为人知。这篇文章将帮助你熟悉用一些流行的语言完成数据科学的工作。

选择 Python 和 GNU Octave 做数据科学工作

我经常尝试学习一种新的编程语言。为什么?这既有对旧方式的厌倦,也有对新方式的好奇。当我开始学习编程时,我唯一知道的语言是 C 语言。那些年的编程生涯既艰难又危险,因为我必须手动分配内存、管理指针、并记得释放内存。

后来一个朋友建议我试试 Python,现在我的编程生活变得轻松多了。虽然程序运行变得慢多了,但我不必通过编写分析软件来受苦了。然而,我很快就意识到每种语言都有比其它语言更适合自己的应用场景。后来我学习了一些其它语言,每种语言都给我带来了一些新的启发。发现新的编程风格让我可以将一些解决方案移植到其他语言中,这样一切都变得有趣多了。

为了对一种新的编程语言(及其文档)有所了解,我总是从编写一些执行我熟悉的任务的示例程序开始。为此,我将解释如何用 Python 和 GNU Octave 编写一个程序来完成一个你可以归类为数据科学的特殊任务。如果你已经熟悉其中一种语言,从它开始,然后通过其他语言寻找相似之处和不同之处。这篇文章并不是对编程语言的详尽比较,只是一个小小的展示。

所有的程序都应该在命令行上运行,而不是用图形用户界面(GUI)。完整的例子可以在 polyglot\_fit 存储库中找到。

编程任务

你将在本系列中编写的程序:

  • CSV 文件中读取数据
  • 用直线插入数据(例如 f(x)=m ⋅ x + q
  • 将结果生成图像文件

这是许多数据科学家遇到的常见情况。示例数据是 Anscombe 的四重奏的第一组,如下表所示。这是一组人工构建的数据,当用直线拟合时会给出相同的结果,但是它们的曲线非常不同。数据文件是一个文本文件,以制表符作为列分隔符,开头几行作为标题。此任务将仅使用第一组(即前两列)。

Python 方式

Python 是一种通用编程语言,是当今最流行的语言之一(依据 TIOBE 指数RedMonk 编程语言排名编程语言流行指数GitHub Octoverse 状态和其他来源的调查结果)。它是一种解释型语言;因此,源代码由执行该指令的程序读取和评估。它有一个全面的标准库并且总体上非常好用(我对这最后一句话没有证据;这只是我的拙见)。

安装

要使用 Python 开发,你需要解释器和一些库。最低要求是:

Fedora 安装它们是很容易的:

sudo dnf install python3 python3-numpy python3-scipy python3-matplotlib

代码注释

在 Python中,注释)是通过在行首添加一个 # 来实现的,该行的其余部分将被解释器丢弃:

# 这是被解释器忽略的注释。

fitting\_python.py 示例使用注释在源代码中插入许可证信息,第一行是特殊注释),它允许该脚本在命令行上执行:

#!/usr/bin/env python3

这一行通知命令行解释器,该脚本需要由程序 python3 执行。

需要的库

在 Python 中,库和模块可以作为一个对象导入(如示例中的第一行),其中包含库的所有函数和成员。可以通过使用 as 方式用自定义标签重命名它们:

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

你也可以决定只导入一个子模块(如第二行和第三行)。语法有两个(基本上)等效的方式:import module.submodulefrom module import submodule

定义变量

Python 的变量是在第一次赋值时被声明的:

input_file_name = "anscombe.csv"
delimiter = "\t"
skip_header = 3
column_x = 0
column_y = 1

变量类型由分配给变量的值推断。没有具有常量值的变量,除非它们在模块中声明并且只能被读取。习惯上,不应被修改的变量应该用大写字母命名。

打印输出

通过命令行运行程序意味着输出只能打印在终端上。Python 有 print() 函数,默认情况下,该函数打印其参数,并在输出的末尾添加一个换行符:

print("#### Anscombe's first set with Python ####")

在 Python 中,可以将 print() 函数与字符串类格式化能力相结合。字符串具有format 方法,可用于向字符串本身添加一些格式化文本。例如,可以添加格式化的浮点数,例如:

print("Slope: {:f}".format(slope))

读取数据

使用 NumPy 和函数 genfromtxt() 读取 CSV 文件非常容易,该函数生成 NumPy 数组

data = np.genfromtxt(input_file_name, delimiter = delimiter, skip_header = skip_header)

在 Python 中,一个函数可以有数量可变的参数,你可以通过指定所需的参数来传递一个参数的子集。数组是非常强大的矩阵状对象,可以很容易地分割成更小的数组:

x = data[:, column_x]
y = data[:, column_y]

冒号选择整个范围,也可以用来选择子范围。例如,要选择数组的前两行,可以使用:

first_two_rows = data[0:1, :]

拟合数据

SciPy 提供了方便的数据拟合功能,例如 linregress() 功能。该函数提供了一些与拟合相关的重要值,如斜率、截距和两个数据集的相关系数:

slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)

print("Slope: {:f}".format(slope))
print("Intercept: {:f}".format(intercept))
print("Correlation coefficient: {:f}".format(r_value))

因为 linregress() 提供了几条信息,所以结果可以同时保存到几个变量中。

绘图

Matplotlib 库仅仅绘制数据点,因此,你应该定义要绘制的点的坐标。已经定义了 xy 数组,所以你可以直接绘制它们,但是你还需要代表直线的数据点。

fit_x = np.linspace(x.min() - 1, x.max() + 1, 100)

linspace() 函数可以方便地在两个值之间生成一组等距值。利用强大的 NumPy 数组可以轻松计算纵坐标,该数组可以像普通数值变量一样在公式中使用:

fit_y = slope * fit_x + intercept

该公式在数组中逐元素应用;因此,结果在初始数组中具有相同数量的条目。

要绘图,首先,定义一个包含所有图形的图形对象

fig_width = 7 #inch
fig_height = fig_width / 16 * 9 #inch
fig_dpi = 100

fig = plt.figure(figsize = (fig_width, fig_height), dpi = fig_dpi)

一个图形可以画几个图;在 Matplotlib 中,这些图被称为。本示例定义一个单轴对象来绘制数据点:

ax = fig.add_subplot(111)

ax.plot(fit_x, fit_y, label = "Fit", linestyle = '-')
ax.plot(x, y, label = "Data", marker = '.', linestyle = '')

ax.legend()
ax.set_xlim(min(x) - 1, max(x) + 1)
ax.set_ylim(min(y) - 1, max(y) + 1)
ax.set_xlabel('x')
ax.set_ylabel('y')

将该图保存到 PNG 图形文件中,有:

fig.savefig('fit_python.png')

如果要显示(而不是保存)该绘图,请调用:

plt.show()

此示例引用了绘图部分中使用的所有对象:它定义了对象 fig 和对象 ax。这在技术上是不必要的,因为 plt 对象可以直接用于绘制数据集。《Matplotlib 教程》展示了这样一个接口:

plt.plot(fit_x, fit_y)

坦率地说,我不喜欢这种方法,因为它隐藏了各种对象之间发生的重要交互。不幸的是,有时官方的例子有点令人困惑,因为他们倾向于使用不同的方法。在这个简单的例子中,引用图形对象是不必要的,但是在更复杂的例子中(例如在图形用户界面中嵌入图形时),引用图形对象就变得很重要了。

结果

命令行输入:

#### Anscombe's first set with Python ####
Slope: 0.500091
Intercept: 3.000091
Correlation coefficient: 0.816421

这是 Matplotlib 产生的图像:

 title=

GNU Octave 方式

GNU Octave 语言主要用于数值计算。它提供了一个简单的操作向量和矩阵的语法,并且有一些强大的绘图工具。这是一种像 Python 一样的解释语言。由于 Octave 的语法几乎兼容 MATLAB,它经常被描述为一个替代 MATLAB 的免费方案。Octave 没有被列为最流行的编程语言,而 MATLAB 则是,所以 Octave 在某种意义上是相当流行的。MATLAB 早于 NumPy,我觉得它是受到了前者的启发。当你看这个例子时,你会看到相似之处。

安装

fitting\_octave.m 的例子只需要基本的 Octave 包,在 Fedora 中安装相当简单:

sudo dnf install octave

代码注释

在 Octave 中,你可以用百分比符号(%)为代码添加注释,如果不需要与 MATLAB 兼容,你也可以使用 #。使用 # 的选项允许你编写像 Python 示例一样的特殊注释行,以便直接在命令行上执行脚本。

必要的库

本例中使用的所有内容都包含在基本包中,因此你不需要加载任何新的库。如果你需要一个库,语法pkg load module。该命令将模块的功能添加到可用功能列表中。在这方面,Python 具有更大的灵活性。

定义变量

变量的定义与 Python 的语法基本相同:

input_file_name = "anscombe.csv";
delimiter = "\t";
skip_header = 3;
column_x = 1;
column_y = 2;

请注意,行尾有一个分号;这不是必需的,但是它会抑制该行结果的输出。如果没有分号,解释器将打印表达式的结果:

octave:1> input_file_name = "anscombe.csv"
input_file_name = anscombe.csv
octave:2> sqrt(2)
ans =  1.4142

打印输出结果

强大的函数 printf() 是用来在终端上打印的。与 Python 不同,printf() 函数不会自动在打印字符串的末尾添加换行,因此你必须添加它。第一个参数是一个字符串,可以包含要传递给函数的其他参数的格式信息,例如:

printf("Slope: %f\n", slope);

在 Python 中,格式是内置在字符串本身中的,但是在 Octave 中,它是特定于 printf() 函数。

读取数据

dlmread() 函数可以读取类似 CSV 文件的文本内容:

data = dlmread(input_file_name, delimiter, skip_header, 0);

结果是一个矩阵对象,这是 Octave 中的基本数据类型之一。矩阵可以用类似于 Python 的语法进行切片:

x = data(:, column_x);
y = data(:, column_y);

根本的区别是索引从 1 开始,而不是从 0 开始。因此,在该示例中,x 列是第一列。

拟合数据

要用直线拟合数据,可以使用 polyfit() 函数。它用一个多项式拟合输入数据,所以你只需要使用一阶多项式:

p = polyfit(x, y, 1);

slope = p(1);
intercept = p(2);

结果是具有多项式系数的矩阵;因此,它选择前两个索引。要确定相关系数,请使用 corr() 函数:

r_value = corr(x, y);

最后,使用 printf() 函数打印结果:

printf("Slope: %f\n", slope);
printf("Intercept: %f\n", intercept);
printf("Correlation coefficient: %f\n", r_value);

绘图

与 Matplotlib 示例一样,首先需要创建一个表示拟合直线的数据集:

fit_x = linspace(min(x) - 1, max(x) + 1, 100);
fit_y = slope * fit_x + intercept;

与 NumPy 的相似性也很明显,因为它使用了 linspace() 函数,其行为就像 Python 的等效版本一样。

同样,与 Matplotlib 一样,首先创建一个对象,然后创建一个对象来保存这些图:

fig_width = 7; %inch
fig_height = fig_width / 16 * 9; %inch
fig_dpi = 100;

fig = figure("units", "inches",
             "position", [1, 1, fig_width, fig_height]);

ax = axes("parent", fig);

set(ax, "fontsize", 14);
set(ax, "linewidth", 2);

要设置轴对象的属性,请使用 set() 函数。然而,该接口相当混乱,因为该函数需要一个逗号分隔的属性和值对列表。这些对只是代表属性名的一个字符串和代表该属性值的第二个对象的连续。还有其他设置各种属性的函数:

xlim(ax, [min(x) - 1, max(x) + 1]);
ylim(ax, [min(y) - 1, max(y) + 1]);
xlabel(ax, 'x');
ylabel(ax, 'y');

绘图是用 plot() 功能实现的。默认行为是每次调用都会重置坐标轴,因此需要使用函数 hold()

hold(ax, "on");

plot(ax, fit_x, fit_y,
     "marker", "none",
     "linestyle", "-",
     "linewidth", 2);
plot(ax, x, y,
     "marker", ".",
     "markersize", 20,
     "linestyle", "none");

hold(ax, "off");

此外,还可以在 plot() 函数中添加属性和值对。legend 必须单独创建,标签应手动声明:

lg = legend(ax, "Fit", "Data");
set(lg, "location", "northwest");

最后,将输出保存到 PNG 图像:

image_size = sprintf("-S%f,%f", fig_width * fig_dpi, fig_height * fig_dpi);
image_resolution = sprintf("-r%f,%f", fig_dpi);

print(fig, 'fit_octave.png',
      '-dpng',
      image_size,
      image_resolution);

令人困惑的是,在这种情况下,选项被作为一个字符串传递,带有属性名和值。因为在 Octave 字符串中没有 Python 的格式化工具,所以必须使用 sprintf() 函数。它的行为就像 printf() 函数,但是它的结果不是打印出来的,而是作为字符串返回的。

在这个例子中,就像在 Python 中一样,图形对象很明显被引用以保持它们之间的交互。如果说 Python 在这方面的文档有点混乱,那么 Octave 的文档就更糟糕了。我发现的大多数例子都不关心引用对象;相反,它们依赖于绘图命令作用于当前活动图形。全局根图形对象跟踪现有的图形和轴。

结果

命令行上的结果输出是:

#### Anscombe's first set with Octave ####
Slope: 0.500091
Intercept: 3.000091
Correlation coefficient: 0.816421

它显示了用 Octave 生成的结果图像。

 title=

接下来

Python 和 GNU Octave 都可以绘制出相同的信息,尽管它们的实现方式不同。如果你想探索其他语言来完成类似的任务,我强烈建议你看看 Rosetta Code。这是一个了不起的资源,可以看到如何用多种语言解决同样的问题。

你喜欢用什么语言绘制数据?在评论中分享你的想法。


via: https://opensource.com/article/20/2/python-gnu-octave-data-science

作者:Cristiano L. Fontana 选题:lujun9972 译者:heguangzhi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

准备好你喜欢的饮料、编辑器和编译器,放一些音乐,然后开始构建一个由多个文件组成的 C 语言程序。

大家常说计算机编程的艺术部分是处理复杂性,部分是命名某些事物。此外,我认为“有时需要添加绘图”是在很大程度上是正确的。

在这篇文章里,我会编写一个小型 C 程序,命名一些东西,同时处理一些复杂性。该程序的结构大致基于我在 《如何写一个好的 C 语言 main 函数》 文中讨论的。但是,这次做一些不同的事。准备好你喜欢的饮料、编辑器和编译器,放一些音乐,让我们一起编写一个有趣的 C 语言程序。

优秀 Unix 程序哲学

首先,你要知道这个 C 程序是一个 Unix 命令行工具。这意味着它运行在(或者可被移植到)那些提供 Unix C 运行环境的操作系统中。当贝尔实验室发明 Unix 后,它从一开始便充满了设计哲学。用我自己的话来说就是:程序只做一件事,并做好它,并且对文件进行一些操作。虽然“只做一件事,并做好它”是有意义的,但是“对文件进行一些操作”的部分似乎有点儿不合适。

事实证明,Unix 中抽象的 “文件” 非常强大。一个 Unix 文件是以文件结束符(EOF)标志为结尾的字节流。仅此而已。文件中任何其它结构均由应用程序所施加而非操作系统。操作系统提供了系统调用,使得程序能够对文件执行一套标准的操作:打开、读取、写入、寻址和关闭(还有其他,但说起来那就复杂了)。对于文件的标准化访问使得不同的程序共用相同的抽象,而且可以一同工作,即使它们是不同的人用不同语言编写的程序。

具有共享的文件接口使得构建可组合的的程序成为可能。一个程序的输出可以作为另一个程序的输入。Unix 家族的操作系统默认在执行程序时提供了三个文件:标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。其中两个文件是只写的:stdoutstderr。而 stdin 是只读的。当我们在常见的 Shell 比如 Bash 中使用文件重定向时,可以看到其效果。

$ ls | grep foo | sed -e 's/bar/baz/g' > ack

这条指令可以被简要地描述为:ls 的结果被写入标准输出,它重定向到 grep 的标准输入,grep 的标准输出重定向到 sed 的标准输入,sed 的标准输出重定向到当前目录下文件名为 ack 的文件中。

我们希望我们的程序在这个灵活又出色的生态系统中运作良好,因此让我们编写一个可以读写文件的程序。

喵呜喵呜:流编码器/解码器概念

当我还是一个露着豁牙的孩子懵懵懂懂地学习计算机科学时,学过很多编码方案。它们中的有些用于压缩文件,有些用于打包文件,另一些毫无用处因此显得十分愚蠢。列举最后这种情况的一个例子:哞哞编码方案

为了让我们的程序有个用途,我为它更新了一个 21 世纪 的概念,并且实现了一个名为“喵呜喵呜” 的编码方案的概念(毕竟网上大家都喜欢猫)。这里的基本的思路是获取文件并且使用文本 “meow” 对每个半字节(半个字节)进行编码。小写字母代表 0,大写字母代表 1。因为它会将 4 个比特替换为 32 个比特,因此会扩大文件的大小。没错,这毫无意义。但是想象一下人们看到经过这样编码后的惊讶表情。

$ cat /home/your_sibling/.super_secret_journal_of_my_innermost_thoughts
MeOWmeOWmeowMEoW...

这非常棒。

最终的实现

完整的源代码可以在 GitHub 上面找到,但是我会写下我在编写程序时的思考。目的是说明如何组织构建多文件 C 语言程序。

既然已经确定了要编写一个编码和解码“喵呜喵呜”格式的文件的程序时,我在 Shell 中执行了以下的命令 :

$ mkdir meowmeow
$ cd meowmeow
$ git init
$ touch Makefile     # 编译程序的方法
$ touch main.c       # 处理命令行选项
$ touch main.h       # “全局”常量和定义
$ touch mmencode.c   # 实现对喵呜喵呜文件的编码
$ touch mmencode.h   # 描述编码 API
$ touch mmdecode.c   # 实现对喵呜喵呜文件的解码
$ touch mmdecode.h   # 描述解码 API
$ touch table.h      # 定义编码查找表
$ touch .gitignore   # 这个文件中的文件名会被 git 忽略
$ git add .
$ git commit -m "initial commit of empty files"

简单的说,我创建了一个目录,里面全是空文件,并且提交到 git。

即使这些文件中没有内容,你依旧可以从它的文件名推断每个文件的用途。为了避免万一你无法理解,我在每条 touch 命令后面进行了简单描述。

通常,程序从一个简单 main.c 文件开始,只有两三个解决问题的函数。然后程序员轻率地向自己的朋友或者老板展示了该程序,然后为了支持所有新的“功能”和“需求”,文件中的函数数量就迅速爆开了。“程序俱乐部”的第一条规则便是不要谈论“程序俱乐部”,第二条规则是尽量减少单个文件中的函数。

老实说,C 编译器并不关心程序中的所有函数是否都在一个文件中。但是我们并不是为计算机或编译器写程序,我们是为其他人(有时也包括我们)去写程序的。我知道这可能有些奇怪,但这就是事实。程序体现了计算机解决问题所采用的一组算法,当问题的参数发生了意料之外的变化时,保证人们可以理解它们是非常重要的。当在人们修改程序时,发现一个文件中有 2049 函数时他们会诅咒你的。

因此,优秀的程序员会将函数分隔开,将相似的函数分组到不同的文件中。这里我用了三个文件 main.cmmencode.cmmdecode.c。对于这样小的程序,也许看起来有些过头了。但是小的程序很难保证一直小下去,因此哥忒拓展做好计划是一个“好主意”。

但是那些 .h 文件呢?我会在后面解释一般的术语,简单地说,它们被称为头文件,同时它们可以包含 C 语言类型定义和 C 预处理指令。头文件中不应该包含任何函数。你可以认为头文件是提供了应用程序接口(API)的定义的一种 .c 文件,可以供其它 .c 文件使用。

但是 Makefile 是什么呢?

我知道下一个轰动一时的应用都是你们这些好孩子们用 “终极代码粉碎者 3000” 集成开发环境来编写的,而构建项目是用 Ctrl-Meta-Shift-Alt-Super-B 等一系列复杂的按键混搭出来的。但是如今(也就是今天),使用 Makefile 文件可以在构建 C 程序时帮助做很多有用的工作。Makefile 是一个包含如何处理文件的方式的文本文件,程序员可以使用其自动地从源代码构建二进制程序(以及其它东西!)

以下面这个小东西为例:

00 # Makefile
01 TARGET= my_sweet_program
02 $(TARGET): main.c
03    cc -o my_sweet_program main.c

# 符号后面的文本是注释,例如 00 行。

01 行是一个变量赋值,将 TARGET 变量赋值为字符串 my_sweet_program。按照惯例,也是我的习惯,所有 Makefile 变量均使用大写字母并用下划线分隔单词。

02 行包含该 步骤 recipe 要创建的文件名和其依赖的文件。在本例中,构建 目标 target my_sweet_program,其依赖是 main.c

最后的 03 行使用了一个制表符号(tab)而不是四个空格。这是将要执行创建目标的命令。在本例中,我们使用 C 编译器 C compiler 前端 cc 以编译链接为 my_sweet_program

使用 Makefile 是非常简单的。

$ make
cc -o my_sweet_program main.c
$ ls
Makefile  main.c  my_sweet_program

构建我们喵呜喵呜编码器/解码器的 Makefile 比上面的例子要复杂,但其基本结构是相同的。我将在另一篇文章中将其分解为 Barney 风格。

形式伴随着功能

我的想法是程序从一个文件中读取、转换它,并将转换后的结果存储到另一个文件中。以下是我想象使用程序命令行交互时的情况:

$ meow < clear.txt > clear.meow
$ unmeow < clear.meow > meow.tx
$ diff clear.txt meow.tx
$

我们需要编写代码以进行命令行解析和处理输入/输出流。我们需要一个函数对流进行编码并将结果写到另一个流中。最后,我们需要一个函数对流进行解码并将结果写到另一个流中。等一下,我们在讨论如何写一个程序,但是在上面的例子中,我调用了两个指令:meowunmeow?我知道你可能会认为这会导致越变越复杂。

次要内容:argv[0] 和 ln 指令

回想一下,C 语言 main 函数的结构如下:

int main(int argc, char *argv[])

其中 argc 是命令行参数的数量,argv 是字符指针(字符串)的列表。argv[0] 是包含正在执行的程序的文件路径。在 Unix 系统中许多互补功能的程序(比如:压缩和解压缩)看起来像两个命令,但事实上,它们是在文件系统中拥有两个名称的一个程序。这个技巧是通过使用 ln 命令创建文件系统链接来实现两个名称的。

在我笔记本电脑中 /usr/bin 的一个例子如下:

$ ls -li /usr/bin/git*
3376 -rwxr-xr-x. 113 root root     1.5M Aug 30  2018 /usr/bin/git
3376 -rwxr-xr-x. 113 root root     1.5M Aug 30  2018 /usr/bin/git-receive-pack
...

这里 gitgit-receive-pack 是同一个文件但是拥有不同的名字。我们说它们是相同的文件因为它们具有相同的 inode 值(第一列)。inode 是 Unix 文件系统的一个特点,对它的介绍超越了本文的内容范畴。

优秀或懒惰的程序可以通过 Unix 文件系统的这个特点达到写更少的代码但是交付双倍的程序。首先,我们编写一个基于其 argv[0] 的值而作出相应改变的程序,然后我们确保为导致该行为的名称创建链接。

在我们的 Makefile 中,unmeow 链接通过以下的方式来创建:

# Makefile
...
$(DECODER): $(ENCODER)
        $(LN) -f $< $@
       ...

我倾向于在 Makefile 中将所有内容参数化,很少使用 “裸” 字符串。我将所有的定义都放置在 Makefile 文件顶部,以便可以简单地找到并改变它们。当你尝试将程序移植到新的平台上时,需要将 cc 改变为某个 cc 时,这会很方便。

除了两个内置变量 $@$< 之外,该 步骤 recipe 看起来相对简单。第一个便是该步骤的目标的快捷方式,在本例中是 $(DECODER)(我能记得这个是因为 @ 符号看起来像是一个目标)。第二个,$< 是规则依赖项,在本例中,它解析为 $(ENCODER)

事情肯定会变得复杂,但它还在管理之中。


via: https://opensource.com/article/19/7/structure-multi-file-c-part-1

作者:Erik O'Shaughnessy 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

让我们看看可以用在你自己的 NLP 应用中的十几个工具吧。

在过去的几年里,自然语言处理(NLP)推动了聊天机器人、语音助手、文本预测等这些渗透到我们的日常生活中的语音或文本应用程技术的发展。目前有着各种各样开源的 NLP 工具,所以我决定调查一下当前开源的 NLP 工具来帮助你制定开发下一个基于语音或文本的应用程序的计划。

尽管我并不熟悉所有工具,但我将从我所熟悉的编程语言出发来介绍这些工具(对于我不熟悉的语言,我无法找到大量的工具)。也就是说,出于各种原因,我排除了三种我熟悉的语言之外的工具。

R 语言可能是没有被包含在内的最重要的语言,因为我发现的大多数库都有一年多没有更新了。这并不一定意味着它们没有得到很好的维护,但我认为它们应该得到更多的更新,以便和同一领域的其他工具竞争。我还选择了最有可能用在生产场景中的语言和工具(而不是在学术界和研究中使用),而我主要是使用 R 作为研究和发现工具。

我也惊讶地发现 Scala 的很多库都没有更新了。我上次使用 Scala 已经过去了两年了,当时它非常流行。但是大多数库从那个时候就再没有更新过,或者只有少数一些有更新。

最后,我排除了 C++。 这主要是因为我上次使用 C++ 编写程序已经有很多年了,而我所工作的组织还没有将 C++ 用于 NLP 或任何数据科学方面的工作。

Python 工具

自然语言工具包(NLTK)

毋庸置疑,自然语言工具包(NLTK)是我调研过的所有工具中功能最完善的一个。它几乎实现了自然语言处理中多数功能组件,比如分类、令牌化、词干化、标注、分词和语义推理。每一个都有多种不同的实现方式,所以你可以选择具体的算法和方式。同时,它也支持不同的语言。然而,它以字符串的形式表示所有的数据,对于一些简单的数据结构来说可能很方便,但是如果要使用一些高级的功能来说就可能有点困难。它的使用文档有点复杂,但也有很多其他人编写的使用文档,比如这本很棒的书。和其他的工具比起来,这个工具库的运行速度有点慢。但总的来说,这个工具包非常不错,可以用于需要具体算法组合的实验、探索和实际应用当中。

SpaCy

SpaCy 可能是 NLTK 的主要竞争者。在大多数情况下都比 NLTK 的速度更快,但是 SpaCy 的每个自然语言处理的功能组件只有一个实现。SpaCy 把所有的东西都表示为一个对象而不是字符串,从而简化了应用构建接口。这也方便它与多种框架和数据科学工具的集成,使得你更容易理解你的文本数据。然而,SpaCy 不像 NLTK 那样支持多种语言。它确实接口简单,具有简化的选项集和完备的文档,以及用于语言处理和分析各种组件的多种神经网络模型。总的来说,对于需要在生产中表现出色且不需要特定算法的新应用程序,这是一个很不错的工具。

TextBlob

TextBlob 是 NLTK 的一个扩展库。你可以通过 TextBlob 用一种更简单的方式来使用 NLTK 的功能,TextBlob 也包括了 Pattern 库中的功能。如果你刚刚开始学习,这将会是一个不错的工具,可以用于对性能要求不太高的生产环境的应用。总体来说,TextBlob 适用于任何场景,但是对小型项目尤佳。

Textacy

这个工具是我用过的名字最好听的。先重读“ex”再带出“cy”,多读“Textacy”几次试试。它不仅仅是名字读起来好,同时它本身也是一个很不错的工具。它使用 SpaCy 作为它自然语言处理核心功能,但它在处理过程的前后做了很多工作。如果你想要使用 SpaCy,那么最好使用 Textacy,从而不用去编写额外的附加代码就可以处理不同种类的数据。

PyTorch-NLP

PyTorch-NLP 才出现短短的一年,但它已经有一个庞大的社区了。它适用于快速原型开发。当出现了最新的研究,或大公司或者研究人员推出了完成新奇的处理任务的其他工具时,比如图像转换,它就会被更新。总体来说,PyTorch 的目标用户是研究人员,但它也能用于原型开发,或使用最先进算法的初始生产载荷中。基于此基础上的创建的库也是值得研究的。

Node.js 工具

Retext

RetextUnified 集合的一部分。Unified 是一个接口,能够集成不同的工具和插件以便它们能够高效的工作。Retext 是 Unified 工具中使用的三种语法之一,另外的两个分别是用于 Markdown 的 Remark 和用于 HTML 的 Rehype。这是一个非常有趣的想法,我很高兴看到这个社区的发展。Retext 没有涉及很多的底层技术,更多的是使用插件去完成你在 NLP 任务中想要做的事情。拼写检查、字形修复、情绪检测和增强可读性都可以用简单的插件来完成。总体来说,如果你不想了解底层处理技术又想完成你的任务的话,这个工具和社区是一个不错的选择。

Compromise

Compromise 显然不是最复杂的工具,如果你正在找拥有最先进的算法和最完备的系统的话,它可能不适合你。然而,如果你想要一个性能好、功能广泛、还能在客户端运行的工具的话,Compromise 值得一试。总体来说,它的名字(“折中”)是准确的,因为作者更关注更具体功能的小软件包,而在功能性和准确性上有所折中,这些小软件包得益于用户对使用环境的理解。

Natural

Natural 包含了常规自然语言处理库所具有的大多数功能。它主要是处理英文文本,但也包括一些其它语言,它的社区也欢迎支持其它的语言。它能够进行令牌化、词干化、分类、语音处理、词频-逆文档频率计算(TF-IDF)、WordNet、字符相似度计算和一些变换。它和 NLTK 有的一比,因为它想要把所有东西都包含在一个包里头,但它更易于使用,而且不一定专注于研究。总的来说,这是一个非常完整的库,目前仍在活跃开发中,但可能需要对底层实现有更多的了解才能完全发挥效力。

Nlp.js

Nlp.js 建立在其他几个 NLP 库之上,包括 Franc 和 Brain.js。它为许多 NLP 组件提供了一个很好的接口,比如分类、情感分析、词干化、命名实体识别和自然语言生成。它也支持一些其它语言,在你处理英语之外的语言时能提供一些帮助。总之,它是一个不错的通用工具,并且提供了调用其他工具的简化接口。在你需要更强大或更灵活的工具之前,这个工具可能会在你的应用程序中用上很长一段时间。

Java 工具

OpenNLP

OpenNLP 是由 Apache 基金会管理的,所以它可以很方便地集成到其他 Apache 项目中,比如 Apache Flink、Apache NiFi 和 Apache Spark。这是一个通用的 NLP 工具,包含了所有 NLP 组件中的通用功能,可以通过命令行或者以包的形式导入到应用中来使用它。它也支持很多种语言。OpenNLP 是一个很高效的工具,包含了很多特性,如果你用 Java 开发生产环境产品的话,它是个很好的选择。

Stanford CoreNLP

Stanford CoreNLP 是一个工具集,提供了统计 NLP、深度学习 NLP 和基于规则的 NLP 功能。这个工具也有许多其他编程语言的版本,所以可以脱离 Java 来使用。它是由高水平的研究机构创建的一个高效的工具,但在生产环境中可能不是最好的。此工具采用双许可证,具有可以用于商业目的的特定许可证。总之,在研究和实验中它是一个很棒的工具,但在生产系统中可能会带来一些额外的成本。比起 Java 版本来说,读者可能对它的 Python 版本更感兴趣。同样,在 Coursera 上最好的机器学习课程之一是斯坦福教授提供的,点此访问其他不错的资源。

CogCompNLP

CogCompNLP 由伊利诺斯大学开发的一个工具,它也有一个相似功能的 Python 版本。它可以用于处理文本,包括本地处理和远程处理,能够极大地缓解你本地设备的压力。它提供了很多处理功能,比如令牌化、词性标注、断句、命名实体标注、词型还原、依存分析和语义角色标注。它是一个很好的研究工具,你可以自己探索它的不同功能。我不确定它是否适合生产环境,但如果你使用 Java 的话,它值得一试。


你最喜欢的开源 NLP 工具和库是什么?请在评论区分享文中没有提到的工具。


via: https://opensource.com/article/19/3/natural-language-processing-tools

作者:Dan Barker 选题:lujun9972 译者:zxp 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

第一篇文章中,我提到,项目的自动部署是放在 now.sh 上,以方便预览。但出于用户体验和速度的考虑,我们选择了国内的七牛云作为页面的承载。不过,七牛毕竟是一个对象存储,而不是一个专业的静态托管业务,在使用上遇到了一些坑,还好经过努力都得到了解决。

七牛的 Bucket 名规则

和绝大多数的云计算厂商一样,七牛也使用了 Bucket 来作为存储的单元。

由于这个项目要挂 Linux.cn 的二级域名,于是我便让老王创建了一个 Bucket,绑定域名,并通过七牛自带的权限控制机制,将其分发给我,让我来使用。

在我的个人控制台看到了这个 Bucket:

发现问题

我通过控制台,手动上传了生成的文件后,确认没有问题,就将相应的功能写入到 Github Action 的 配置文件(配置文件点这里)中,实现自动的部署。但在部署过程中,屡次报错,不知道为什么。在开启了 DEBUG 信息后发现,竟然是 Bucket 不存在(点我查看 CI 的构建信息)。

解决问题

和老王沟通以后才发现,是七牛的 Bucket 名机制的问题。

在七牛中进行权限分配的时候,会要求你为 Bucket 设定一个别名,而且名字和已有的名字必须是不同的,这导致我看到的 Bucket 的名和老王创建的 Bucket 名是不相同的。

而我使用的 AK 和 SK 又是让老王设置在 Github 后台的 Secrets,Bucket 则是我自己设置的,所以就出现了问题。具体来说,是下面这张图。

由于我填写的 Bucket 是我自己看到的,而不是老王那边真正的 Bucket 名称,导致在上传的时候,无法找到 Bucket。在将 Bucket 名称替换为老王那边看到的 Bucket 名称后,问题得到解决。

七牛不支持 Vue Router 的 History 模式

第二篇文章中,我提到了引入了 Vue 的 History 模式来优化体验。但是,七牛本身作为一个存储系统,没有转发的功能,也就导致其没有办法很好的支持 Vue History 模式。

在经过一番研究后,找到了解决方案,就是将 index 页面,同时作为 404 页面,这样就可以实现从某种意义上的将所有请求都转发给 Index 页面。

你需要做的,就是将 index.html 复制一份,并重命名为 errno-404,并和其他文件一同上传,这样用户请求一些不存在的文件时,会自动将请求转发给 errno-404, 又因为这个文件的内容是索引文件的内容,所以就可以实现了请求的转发。

相关代码的实现,你可以在 https://github.com/LCTT/tldr.linux.cn/blob/master/.github/workflows/nodejs.yml 这里找到。

总结

在这篇文章中,介绍了七牛的 Bucket 问题,以及 Vue Router History 模式在七牛下的解决方案。

学习如何使用 Java 8 中的流 API 和函数式编程结构。

当 Java SE 8(又名核心 Java 8)在 2014 年被推出时,它引入了一些更改,从根本上影响了用它进行的编程。这些更改中有两个紧密相连的部分:流 API 和函数式编程构造。本文使用代码示例,从基础到高级特性,介绍每个部分并说明它们之间的相互作用。

基础特性

流 API 是在数据序列中迭代元素的简洁而高级的方法。包 java.util.streamjava.util.function 包含了用于流 API 和相关函数式编程构造的新库。当然,代码示例胜过千言万语。

下面的代码段用大约 2,000 个随机整数值填充了一个 List

Random rand = new Random2();
List<Integer> list = new ArrayList<Integer>();           // 空 list
for (int i = 0; i < 2048; i++) list.add(rand.nextInt()); // 填充它

另外用一个 for 循环可用于遍历填充列表,以将偶数值收集到另一个列表中。

流 API 提供了一种更简洁的方法来执行此操作:

List <Integer> evens = list
    .stream()                      // 流化 list
    .filter(n -> (n & 0x1) == 0)   // 过滤出奇数值
    .collect(Collectors.toList()); // 收集偶数值

这个例子有三个来自流 API 的函数:

  • stream 函数可以将集合转换为流,而流是一个每次可访问一个值的传送带。流化是惰性的(因此也是高效的),因为值是根据需要产生的,而不是一次性产生的。
  • filter 函数确定哪些流的值(如果有的话)通过了处理管道中的下一个阶段,即 collect 阶段。filter 函数是 高阶的 higher-order ,因为它的参数是一个函数 —— 在这个例子中是一个 lambda 表达式,它是一个未命名的函数,并且是 Java 新的函数式编程结构的核心。

lambda 语法与传统的 Java 完全不同:

n -> (n & 0x1) == 0

箭头(一个减号后面紧跟着一个大于号)将左边的参数列表与右边的函数体分隔开。参数 n 虽未明确类型,但也可以明确。在任何情况下,编译器都会发现 n 是个 Integer。如果有多个参数,这些参数将被括在括号中,并用逗号分隔。

在本例中,函数体检查一个整数的最低位(最右)是否为零,这用来表示偶数。过滤器应返回一个布尔值。尽管可以,但该函数的主体中没有显式的 return。如果主体没有显式的 return,则主体的最后一个表达式即是返回值。在这个例子中,主体按照 lambda 编程的思想编写,由一个简单的布尔表达式 (n & 0x1) == 0 组成。

  • collect 函数将偶数值收集到引用为 evens 的列表中。如下例所示,collect 函数是线程安全的,因此,即使在多个线程之间共享了过滤操作,该函数也可以正常工作。

方便的功能和轻松实现多线程

在生产环境中,数据流的源可能是文件或网络连接。为了学习流 API, Java 提供了诸如 IntStream 这样的类型,它可以用各种类型的元素生成流。这里有一个 IntStream 的例子:

IntStream                          // 整型流
    .range(1, 2048)                // 生成此范围内的整型流
    .parallel()                    // 为多个线程分区数据
    .filter(i -> ((i & 0x1) > 0))  // 奇偶校验 - 只允许奇数通过
    .forEach(System.out::println); // 打印每个值

IntStream 类型包括一个 range 函数,该函数在指定的范围内生成一个整数值流,在本例中,以 1 为增量,从 1 递增到 2048。parallel 函数自动划分该工作到多个线程中,在各个线程中进行过滤和打印。(线程数通常与主机系统上的 CPU 数量匹配。)函数 forEach 参数是一个方法引用,在本例中是对封装在 System.out 中的 println 方法的引用,方法输出类型为 PrintStream。方法和构造器引用的语法将在稍后讨论。

由于具有多线程,因此整数值整体上以任意顺序打印,但在给定线程中是按顺序打印的。例如,如果线程 T1 打印 409 和 411,那么 T1 将按照顺序 409-411 打印,但是其它某个线程可能会预先打印 2045。parallel 调用后面的线程是并发执行的,因此它们的输出顺序是不确定的。

map/reduce 模式

map/reduce 模式在处理大型数据集方面变得很流行。一个 map/reduce 宏操作由两个微操作构成。首先,将数据分散( 映射 mapped )到各个工作程序中,然后将单独的结果收集在一起 —— 也可能收集统计起来成为一个值,即 归约 reduction 。归约可以采用不同的形式,如以下示例所示。

下面 Number 类的实例用 EVENODD 表示有奇偶校验的整数值:

public class Number {
    enum Parity { EVEN, ODD }
    private int value;
    public Number(int n) { setValue(n); }
    public void setValue(int value) { this.value = value; }
    public int getValue() { return this.value; }
    public Parity getParity() {
        return ((value & 0x1) == 0) ? Parity.EVEN : Parity.ODD;
    }
    public void dump() {
        System.out.format("Value: %2d (parity: %s)\n", getValue(),
                          (getParity() == Parity.ODD ? "odd" : "even"));
    }
}

下面的代码演示了用 Number 流进行 map/reduce 的情形,从而表明流 API 不仅可以处理 intfloat 等基本类型,还可以处理程序员自定义的类类型。

在下面的代码段中,使用了 parallelStream 而不是 stream 函数对随机整数值列表进行流化处理。与前面介绍的 parallel 函数一样,parallelStream 变体也可以自动执行多线程。

final int howMany = 200;
Random r = new Random();
Number[] nums = new Number[howMany];
for (int i = 0; i < howMany; i++) nums[i] = new Number(r.nextInt(100));
List<Number> listOfNums = Arrays.asList(nums);  // 将数组转化为 list

Integer sum4All = listOfNums
    .parallelStream()           // 自动执行多线程
    .mapToInt(Number::getValue) // 使用方法引用,而不是 lambda
    .sum();                     // 将流值计算出和值
System.out.println("The sum of the randomly generated values is: " + sum4All);

高阶的 mapToInt 函数可以接受一个 lambda 作为参数,但在本例中,它接受一个方法引用,即 Number::getValuegetValue 方法不需要参数,它返回给定的 Number 实例的 int 值。语法并不复杂:类名 Number 后跟一个双冒号和方法名。回想一下先前的例子 System.out::println,它在 System 类中的 static 属性 out 后面有一个双冒号。

方法引用 Number::getValue 可以用下面的 lambda 表达式替换。参数 n 是流中的 Number 实例中的之一:

mapToInt(n -> n.getValue())

通常,lambda 表达式和方法引用是可互换的:如果像 mapToInt 这样的高阶函数可以采用一种形式作为参数,那么这个函数也可以采用另一种形式。这两个函数式编程结构具有相同的目的 —— 对作为参数传入的数据执行一些自定义操作。在两者之间进行选择通常是为了方便。例如,lambda 可以在没有封装类的情况下编写,而方法则不能。我的习惯是使用 lambda,除非已经有了适当的封装方法。

当前示例末尾的 sum 函数通过结合来自 parallelStream 线程的部分和,以线程安全的方式进行归约。但是,程序员有责任确保在 parallelStream 调用引发的多线程过程中,程序员自己的函数调用(在本例中为 getValue)是线程安全的。

最后一点值得强调。lambda 语法鼓励编写 纯函数 pure function ,即函数的返回值仅取决于传入的参数(如果有);纯函数没有副作用,例如更新一个类中的 static 字段。因此,纯函数是线程安全的,并且如果传递给高阶函数的函数参数(例如 filtermap )是纯函数,则流 API 效果最佳。

对于更细粒度的控制,有另一个流 API 函数,名为 reduce,可用于对 Number 流中的值求和:

Integer sum4AllHarder = listOfNums
    .parallelStream()                           // 多线程
    .map(Number::getValue)                      // 每个 Number 的值
    .reduce(0, (sofar, next) -> sofar + next);  // 求和

此版本的 reduce 函数带有两个参数,第二个参数是一个函数:

  • 第一个参数(在这种情况下为零)是特征值,该值用作求和操作的初始值,并且在求和过程中流结束时用作默认值。
  • 第二个参数是累加器,在本例中,这个 lambda 表达式有两个参数:第一个参数(sofar)是正在运行的和,第二个参数(next)是来自流的下一个值。运行的和以及下一个值相加,然后更新累加器。请记住,由于开始时调用了 parallelStream,因此 mapreduce 函数现在都在多线程上下文中执行。

在到目前为止的示例中,流值被收集,然后被规约,但是,通常情况下,流 API 中的 Collectors 可以累积值,而不需要将它们规约到单个值。正如下一个代码段所示,收集活动可以生成任意丰富的数据结构。该示例使用与前面示例相同的 listOfNums

Map<Number.Parity, List<Number>> numMap = listOfNums
    .parallelStream()
    .collect(Collectors.groupingBy(Number::getParity));

List<Number> evens = numMap.get(Number.Parity.EVEN);
List<Number> odds = numMap.get(Number.Parity.ODD);

第一行中的 numMap 指的是一个 Map,它的键是一个 Number 奇偶校验位(ODDEVEN),其值是一个具有指定奇偶校验位值的 Number 实例的 List。同样,通过 parallelStream 调用进行多线程处理,然后 collect 调用(以线程安全的方式)将部分结果组装到 numMap 引用的 Map 中。然后,在 numMap 上调用 get 方法两次,一次获取 evens,第二次获取 odds

实用函数 dumpList 再次使用来自流 API 的高阶 forEach 函数:

private void dumpList(String msg, List<Number> list) {
    System.out.println("\n" + msg);
    list.stream().forEach(n -> n.dump()); // 或者使用 forEach(Number::dump)
}

这是示例运行中程序输出的一部分:

The sum of the randomly generated values is: 3322
The sum again, using a different method:     3322

Evens:

Value: 72 (parity: even)
Value: 54 (parity: even)
...
Value: 92 (parity: even)

Odds:

Value: 35 (parity: odd)
Value: 37 (parity: odd)
...
Value: 41 (parity: odd)

用于代码简化的函数式结构

函数式结构(如方法引用和 lambda 表达式)非常适合在流 API 中使用。这些构造代表了 Java 中对高阶函数的主要简化。即使在糟糕的过去,Java 也通过 MethodConstructor 类型在技术上支持高阶函数,这些类型的实例可以作为参数传递给其它函数。由于其复杂性,这些类型在生产级 Java 中很少使用。例如,调用 Method 需要对象引用(如果方法是非静态的)或至少一个类标识符(如果方法是静态的)。然后,被调用的 Method 的参数作为对象实例传递给它,如果没有发生多态(那会出现另一种复杂性!),则可能需要显式向下转换。相比之下,lambda 和方法引用很容易作为参数传递给其它函数。

但是,新的函数式结构在流 API 之外具有其它用途。考虑一个 Java GUI 程序,该程序带有一个供用户按下的按钮,例如,按下以获取当前时间。按钮按下的事件处理程序可能编写如下:

JButton updateCurrentTime = new JButton("Update current time");
updateCurrentTime.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        currentTime.setText(new Date().toString());
    }
});

这个简短的代码段很难解释。关注第二行,其中方法 addActionListener 的参数开始如下:

new ActionListener() {

这似乎是错误的,因为 ActionListener 是一个抽象接口,而抽象类型不能通过调用 new 实例化。但是,事实证明,还有其它一些实例被实例化了:一个实现此接口的未命名内部类。如果上面的代码封装在名为 OldJava 的类中,则该未命名的内部类将被编译为 OldJava$1.classactionPerformed 方法在这个未命名的内部类中被重写。

现在考虑使用新的函数式结构进行这个令人耳目一新的更改:

updateCurrentTime.addActionListener(e -> currentTime.setText(new Date().toString()));

lambda 表达式中的参数 e 是一个 ActionEvent 实例,而 lambda 的主体是对按钮上的 setText 的简单调用。

函数式接口和函数组合

到目前为止,使用的 lambda 已经写好了。但是,为了方便起见,我们可以像引用封装方法一样引用 lambda 表达式。以下一系列简短示例说明了这一点。

考虑以下接口定义:

@FunctionalInterface // 可选,通常省略
interface BinaryIntOp {
    abstract int compute(int arg1, int arg2); // abstract 声明可以被删除
}

注释 @FunctionalInterface 适用于声明唯一抽象方法的任何接口;在本例中,这个抽象接口是 compute。一些标准接口,(例如具有唯一声明方法 runRunnable 接口)同样符合这个要求。在此示例中,compute 是已声明的方法。该接口可用作引用声明中的目标类型:

BinaryIntOp div = (arg1, arg2) -> arg1 / arg2;
div.compute(12, 3); // 4

java.util.function 提供各种函数式接口。以下是一些示例。

下面的代码段介绍了参数化的 Predicate 函数式接口。在此示例中,带有参数 StringPredicate<String> 类型可以引用具有 String 参数的 lambda 表达式或诸如 isEmpty 之类的 String 方法。通常情况下,Predicate 是一个返回布尔值的函数。

Predicate<String> pred = String::isEmpty; // String 方法的 predicate 声明
String[] strings = {"one", "two", "", "three", "four"};
Arrays.asList(strings)
   .stream()
   .filter(pred)                  // 过滤掉非空字符串
   .forEach(System.out::println); // 只打印空字符串

在字符串长度为零的情况下,isEmpty Predicate 判定结果为 true。 因此,只有空字符串才能进入管道的 forEach 阶段。

下一段代码将演示如何将简单的 lambda 或方法引用组合成更丰富的 lambda 或方法引用。考虑这一系列对 IntUnaryOperator 类型的引用的赋值,它接受一个整型参数并返回一个整型值:

IntUnaryOperator doubled = n -> n * 2;
IntUnaryOperator tripled = n -> n * 3;
IntUnaryOperator squared = n -> n * n;

IntUnaryOperator 是一个 FunctionalInterface,其唯一声明的方法为 applyAsInt。现在可以单独使用或以各种组合形式使用这三个引用 doubledtripledsquared

int arg = 5;
doubled.applyAsInt(arg); // 10
tripled.applyAsInt(arg); // 15
squared.applyAsInt(arg); // 25

以下是一些函数组合的样例:

int arg = 5;
doubled.compose(squared).applyAsInt(arg); // 5 求 2 次方后乘 2:50
tripled.compose(doubled).applyAsInt(arg); // 5 乘 2 后再乘 3:30
doubled.andThen(squared).applyAsInt(arg); // 5 乘 2 后求 2 次方:100
squared.andThen(tripled).applyAsInt(arg); // 5 求 2 次方后乘 3:75

函数组合可以直接使用 lambda 表达式实现,但是引用使代码更简洁。

构造器引用

构造器引用是另一种函数式编程构造,而这些引用在比 lambda 和方法引用更微妙的上下文中非常有用。再一次重申,代码示例似乎是最好的解释方式。

考虑这个 POJO 类:

public class BedRocker { // 基岩的居民
    private String name;
    public BedRocker(String name) { this.name = name; }
    public String getName() { return this.name; }
    public void dump() { System.out.println(getName()); }
}

该类只有一个构造函数,它需要一个 String 参数。给定一个名字数组,目标是生成一个 BedRocker 元素数组,每个名字代表一个元素。下面是使用了函数式结构的代码段:

String[] names = {"Fred", "Wilma", "Peebles", "Dino", "Baby Puss"};

Stream<BedRocker> bedrockers = Arrays.asList(names).stream().map(BedRocker::new);
BedRocker[] arrayBR = bedrockers.toArray(BedRocker[]::new);

Arrays.asList(arrayBR).stream().forEach(BedRocker::dump);

在较高的层次上,这个代码段将名字转换为 BedRocker 数组元素。具体来说,代码如下所示。Stream 接口(在包 java.util.stream 中)可以被参数化,而在本例中,生成了一个名为 bedrockersBedRocker 流。

Arrays.asList 实用程序再次用于流化一个数组 names,然后将流的每一项传递给 map 函数,该函数的参数现在是构造器引用 BedRocker::new。这个构造器引用通过在每次调用时生成和初始化一个 BedRocker 实例来充当一个对象工厂。在第二行执行之后,名为 bedrockers 的流由五项 BedRocker 组成。

这个例子可以通过关注高阶 map 函数来进一步阐明。在通常情况下,一个映射将一个类型的值(例如,一个 int)转换为另一个相同类型的值(例如,一个整数的后继):

map(n -> n + 1) // 将 n 映射到其后继

然而,在 BedRocker 这个例子中,转换更加戏剧化,因为一个类型的值(代表一个名字的 String)被映射到一个不同类型的值,在这个例子中,就是一个 BedRocker 实例,这个字符串就是它的名字。转换是通过一个构造器调用来完成的,它是由构造器引用来实现的:

map(BedRocker::new) // 将 String 映射到 BedRocker

传递给构造器的值是 names 数组中的其中一项。

此代码示例的第二行还演示了一个你目前已经非常熟悉的转换:先将数组先转换成 List,然后再转换成 Stream

Stream<BedRocker> bedrockers = Arrays.asList(names).stream().map(BedRocker::new);

第三行则是另一种方式 —— 流 bedrockers 通过使用数组构造器引用 BedRocker[]::new 调用 toArray 方法:

BedRocker[ ] arrayBR = bedrockers.toArray(BedRocker[]::new);

该构造器引用不会创建单个 BedRocker 实例,而是创建这些实例的整个数组:该构造器引用现在为 BedRocker[]:new,而不是 BedRocker::new。为了进行确认,将 arrayBR 转换为 List,再次对其进行流式处理,以便可以使用 forEach 来打印 BedRocker 的名字。

Fred
Wilma
Peebles
Dino
Baby Puss

该示例对数据结构的微妙转换仅用几行代码即可完成,从而突出了可以将 lambda,方法引用或构造器引用作为参数的各种高阶函数的功能。

柯里化 Currying

柯里化函数是指减少函数执行任何工作所需的显式参数的数量(通常减少到一个)。(该术语是为了纪念逻辑学家 Haskell Curry。)一般来说,函数的参数越少,调用起来就越容易,也更健壮。(回想一下一些需要半打左右参数的噩梦般的函数!)因此,应将柯里化视为简化函数调用的一种尝试。java.util.function 包中的接口类型适合于柯里化,如以下示例所示。

引用的 IntBinaryOperator 接口类型是为函数接受两个整型参数,并返回一个整型值:

IntBinaryOperator mult2 = (n1, n2) -> n1 * n2;
mult2.applyAsInt(10, 20); // 200
mult2.applyAsInt(10, 30); // 300

引用 mult2 强调了需要两个显式参数,在本例中是 10 和 20。

前面介绍的 IntUnaryOperatorIntBinaryOperator 简单,因为前者只需要一个参数,而后者则需要两个参数。两者均返回整数值。因此,目标是将名为 mult2 的两个参数 IntBinraryOperator 柯里化成一个单一的 IntUnaryOperator 版本 curriedMult2

考虑 IntFunction<R> 类型。此类型的函数采用整型参数,并返回类型为 R 的结果,该结果可以是另一个函数 —— 更准确地说,是 IntBinaryOperator。让一个 lambda 返回另一个 lambda 很简单:

arg1 -> (arg2 -> arg1 * arg2) // 括号可以省略

完整的 lambda 以 arg1 开头,而该 lambda 的主体以及返回的值是另一个以 arg2 开头的 lambda。返回的 lambda 仅接受一个参数(arg2),但返回了两个数字的乘积(arg1arg2)。下面的概述,再加上代码,应该可以更好地进行说明。

以下是如何柯里化 mult2 的概述:

  • 类型为 IntFunction<IntUnaryOperator> 的 lambda 被写入并调用,其整型值为 10。返回的 IntUnaryOperator 缓存了值 10,因此变成了已柯里化版本的 mult2,在本例中为 curriedMult2
  • 然后使用单个显式参数(例如,20)调用 curriedMult2 函数,该参数与缓存的参数(在本例中为 10)相乘以生成返回的乘积。。

这是代码的详细信息:

// 创建一个接受一个参数 n1 并返回一个单参数 n2 -> n1 * n2 的函数,该函数返回一个(n1 * n2 乘积的)整型数。
IntFunction<IntUnaryOperator> curriedMult2Maker = n1 -> (n2 -> n1 * n2);

调用 curriedMult2Maker 生成所需的 IntUnaryOperator 函数:

// 使用 curriedMult2Maker 获取已柯里化版本的 mult2。
// 参数 10 是上面的 lambda 的 n1。
IntUnaryOperator curriedMult2 = curriedMult2Maker2.apply(10);

10 现在缓存在 curriedMult2 函数中,以便 curriedMult2 调用中的显式整型参数乘以 10:

curriedMult2.applyAsInt(20); // 200 = 10 * 20
curriedMult2.applyAsInt(80); // 800 = 10 * 80

缓存的值可以随意更改:

curriedMult2 = curriedMult2Maker.apply(50); // 缓存 50
curriedMult2.applyAsInt(101);               // 5050 = 101 * 50

当然,可以通过这种方式创建多个已柯里化版本的 mult2,每个版本都有一个 IntUnaryOperator

柯里化充分利用了 lambda 的强大功能:可以很容易地编写 lambda 表达式来返回需要的任何类型的值,包括另一个 lambda。

总结

Java 仍然是基于类的面向对象的编程语言。但是,借助流 API 及其支持的函数式构造,Java 向函数式语言(例如 Lisp)迈出了决定性的(同时也是受欢迎的)一步。结果是 Java 更适合处理现代编程中常见的海量数据流。在函数式方向上的这一步还使以在前面的代码示例中突出显示的管道的方式编写清晰简洁的 Java 代码更加容易:

dataStream
   .parallelStream() // 多线程以提高效率
   .filter(...)      // 阶段 1
   .map(...)         // 阶段 2
   .filter(...)      // 阶段 3
   ...
   .collect(...);    // 或者,也可以进行归约:阶段 N

自动多线程,以 parallelparallelStream 调用为例,建立在 Java 的 fork/join 框架上,该框架支持 任务窃取 task stealing 以提高效率。假设 parallelStream 调用后面的线程池由八个线程组成,并且 dataStream 被八种方式分区。某个线程(例如,T1)可能比另一个线程(例如,T7)工作更快,这意味着应该将 T7 的某些任务移到 T1 的工作队列中。这会在运行时自动发生。

在这个简单的多线程世界中,程序员的主要职责是编写线程安全函数,这些函数作为参数传递给在流 API 中占主导地位的高阶函数。尤其是 lambda 鼓励编写纯函数(因此是线程安全的)函数。


via: https://opensource.com/article/20/1/javastream

作者:Marty Kalin 选题:lujun9972 译者:laingke 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出