·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 5 月 28 日
--
图片由 Jose Castillo 提供,来源于 Unsplash
注意: 以下所有示例代码片段均由本文作者编写。
算法思维 是将严谨的逻辑和创造力结合起来,用以框定、解决和分析问题,通常借助计算机的帮助。涉及某种排序、查找和优化形式的问题与算法思维密切相关,且常出现在数据科学项目中。算法思维帮助我们以高效利用时间和空间(例如计算机的磁盘空间或内存)的方式解决这些问题,从而得到快速且节省资源的算法。
即使在可预见的未来,存储和计算的成本持续下降,算法思维在数据科学项目中的重要性仍然不会因为以下几个关键原因而减弱。首先,客户的需求往往超过了现有解决方案的能力,在许多商业应用场景中,无论数据科学管道的底层复杂性如何(从数据获取、转换到建模和供应),这一点都成立。客户期望那些需要数天或数小时才能完成的任务变得只需要几分钟或几秒钟,而那些需要几分钟或几秒钟的任务则希望能在眨眼间完成。第二,越来越多的应用场景涉及设备端分析(例如,在嵌入式系统、物联网和边缘计算的背景下),这要求计算资源更加高效;存储空间和内存非常紧张,可能无法将计算任务转移到云端更强大、更集中的基础设施上。第三,工业数据科学管道的运行可能会消耗大量能源,从而加剧持续的气候危机。深入理解算法思维可以帮助数据科学家构建高效且可持续的解决方案,以应对这些挑战。
尽管拥有计算机科学学位的数据科学家会熟悉算法思维的核心概念,但越来越多的人以其他背景进入该领域,这些背景涵盖了自然科学、社会科学以及艺术等多个领域;随着生成性人工智能的进步和数据科学在学校及大学课程中日益普及,这一趋势在未来几年可能会加速。因此,本文接下来的章节主要面向那些不熟悉算法思维的读者。我们将从算法问题解决过程的高层次概述开始,然后通过研究一些发布在HackerRank上的编程挑战(一个广泛被公司用于招聘数据科学家的平台)以动手的方式帮助读者逐步建立算法思维的直觉。我们还将介绍一些有用的进一步阅读资源。最后,我们将简要讨论算法思维在 AI 辅助软件开发中的相关性(例如,使用 GitHub Copilot),并作总结。
本节的标题也是一本著名书籍的标题,该书最早由匈牙利裔美国数学家、斯坦福大学教授乔治·波利亚于 1945 年出版。在*《如何解题》*(link)中,波利亚提出了一个看似简单但极为有效的四步法,适用于算法问题解决:
-
理解问题:仔细框定问题,充分考虑问题和解决方案空间的任何限制(例如,允许的输入数据类型和数据范围、输出格式、最大执行时间)。提出类似“我能否用自己的话重新陈述问题?”和“我是否拥有足够的数据来实施一个有用的解决方案?”等问题,以检查自己的理解。使用具体的例子(或数据集)使问题及其边缘情况更加具体。花足够的时间在这一步通常能让后续步骤更容易执行。
-
制定计划:这通常涉及将问题分解为更小的子问题,对于这些子问题,可能已经有了有效的解决方案。识别和应用合适的现有解决方案来处理不同类型的子问题(例如,在搜索、排序等方面)是通过实践和经验获得的。但有时,可能需要额外的创造力,将多个现有方法结合,发明新的方法,或者借用其他领域的解决方案并通过类比加以应用。Pólya 提出了几个帮助思考过程的建议,例如画图并从期望目标反向推理。通常,在这一阶段,至少从高层次判断所制定的计划是否能够解决指定问题是很有用的。
-
执行计划:使用相关工具实施解决方案。在数据科学项目中,这可能涉及使用诸如 scikit-learn、PyTorch 和 TensorFlow 等机器学习库,以及 AWS、GCP 或 Azure 等平台来托管和运行流水线。在这一阶段,注重细节至关重要,因为即使是代码中的小错误也可能导致实现结果无法准确反映之前制定的计划,从而无法解决所述问题。添加足够的单元测试,以检查代码的不同部分是否正常工作,即使是边缘情况也要考虑在内。
-
回顾:“回顾”是大多数数据科学项目验证阶段的本能部分;例如,“新的机器学习模型是否比之前的更好?”这样的问题,只有通过收集和审查每个实验的相关指标才能回答。但是,回顾数据科学流水线的其他方面(例如,ETL 代码、测试用例、产品化脚本)和人工智能生命周期管理(例如,自动化水平、数据隐私和安全性、生产中的反馈环实施)同样至关重要,这有助于改进当前项目并在未来的项目中做得更好,即使在快速节奏的工作环境中,找到时间进行这种全面的“回顾”可能会有挑战。
在波利亚问题解决过程中,步骤 1 和步骤 2 特别难以正确完成。以概念上逻辑且系统的方式框定问题或解决方案通常是一项复杂的任务。然而,熟悉 概念框架(用于表示抽象概念的分析结构)可以大大帮助解决这个问题。常见的概念框架包括树形图、矩阵、流程图和关系图。由本文作者编写的书籍 Conceptual Frameworks: A Guide to Structuring Analyses, Decisions and Presentations (链接)介绍了如何以易于理解的方式理解、创建、应用和评估这些概念框架。
在算法问题求解的背景下,有一个值得特别注意的话题,那就是 复杂度。在比较两个不同的算法时,考虑每个算法的时间和空间复杂度是非常有用的,即每个算法在相对于问题规模(或数据规模)的时间和空间消耗如何变化。你应该了解五种基本的复杂度等级,从最低(最好)到最高(最差)。为了简化讨论,下面我们仅从时间复杂度的角度描述它们:
-
瞬时:无论问题规模如何,算法都能瞬间执行。例如,要判断一个整数是否为偶数,我们可以简单地检查它的最右边的数字是否能被二整除,无论这个整数有多大。通过索引访问列表元素通常也可以瞬间完成,不管列表的长度如何。
-
对数级:对于大小为 n 的数据集,算法执行大约 log(n) 步骤。请注意,对数的底数可能不同(例如,log2(n) 用于二分查找,因为每次迭代都将问题的大小减半)。像瞬时算法一样,具有对数复杂度的算法也很有吸引力,因为它们相对于问题规模是次线性扩展的。
-
线性:顾名思义,对于大小为 n 的数据集,具有线性复杂度的算法大约执行 n 步。
-
多项式:该算法的执行时间是 x²(二次)、x³(三次)或更一般的 x^m 步骤,其中 m 为某个正整数。检查代码中多项式复杂度的常见方法是计数嵌套循环的数量;例如,包含两个嵌套循环(循环内有循环)的函数,其复杂度为 x²,包含三个嵌套循环的函数复杂度为 x³,依此类推。
-
指数级:算法执行的时间步长为2^x,3^x,或者更一般地,m^x,其中m是某个正整数。请参考 StackExchange 上的这些帖子(链接 1,链接 2),了解为什么指数函数最终会比多项式函数增长更快,因此在处理大规模问题时,指数函数的算法复杂度会更差。
一些算法可能会表现出加法或乘法的组合复杂度。例如,一个 for 循环后接二分查找会导致线性和对数复杂度的加法组合,这是由于循环和查找过程分别是顺序执行的。相比之下,如果每次迭代中都进行二分查找,那么将会是线性和对数复杂度的乘法组合。虽然乘法组合通常比加法组合开销更大,但有时是不可避免的,且仍然可以进行优化。例如,像归并排序这样的排序算法,其时间复杂度为nlog(n),比选择排序的二次时间复杂度要便宜(参见这篇文章,里面有不同排序算法复杂度的对比表)。
在接下来的内容中,我们将研究在HackerRank上发布的一些问题。类似的问题也可以在LeetCode和CodeWars等平台上找到。研究这些平台上发布的问题有助于训练你的算法思维能力,可以帮助你更轻松地应对技术面试(招聘经理通常会向申请数据科学岗位的候选人提出算法问题),并且可能会产出一些可以在工作中复用的代码。
以下所有的示例代码片段均由本文作者编写,使用了 C++,这是一种在构建快速数据管道时常被从业者选择的语言。根据需要,这些代码片段也可以很容易地转化为其他语言,如 Python 或 R。为了简化代码片段,我们假设代码文件的顶部包含以下几行:
#include <bits/stdc++.h>
using namespace std;
这将使我们能够在代码中省略“std::”,从而让读者更专注于算法本身。当然,在实际的 C++代码中,只有相关的库会被包含,且“std::”会按照编码规范显式写出。
一个最初看起来需要通过多项式复杂度的迭代解法(例如,使用 for 循环、while 循环或列表推导式)的问题,有时可以通过一个公式在瞬间得出所需的答案,从而避免复杂的迭代。
考虑数字线跳跃问题(链接)。有两只袋鼠被放置在数字线上某个位置(分别在位置x1和x2),并且可以通过跳跃移动。第一只袋鼠每次跳跃可以移动v1米,而第二只袋鼠每次跳跃可以移动v2米。给定x1、v1、x2和v2的输入值,任务是确定是否有可能在未来的某个时间步,两个袋鼠会出现在数字线上的相同位置,假设每只袋鼠每个时间步只能跳一次;解决函数应相应地返回“YES”或“NO”。
假设x1小于x2。一种方法是实现一个循环,检查从x1出发的袋鼠是否会追上从x2出发的袋鼠。换句话说,我们会检查是否存在一个正的(整数)时间步长,使得x1 + v1t = x2 + v2t。如果x1大于x2,我们可以交换相应变量中的值,并遵循上述相同的方法。但是,如果t很大,这样的解决方案可能需要很长时间才能执行,甚至如果袋鼠永远不会相遇,它可能会导致无限循环(导致超时或崩溃)。
我们可以做得更好。让我们重新排列上面的方程,解出正整数t。我们得到t = (x1 — x2)/(v2 — v1)。当v2 = v1时,这个t方程是未定义的(由于除以零),但在这种情况下,如果两只袋鼠起始位置相同,我们可以返回“YES”,因为两只袋鼠显然会在下一个时间步到达数字线上的相同位置。此外,如果两只袋鼠的跳跃距离相同,但起始位置不同,那么我们可以直接返回“NO”,因为从左边出发的袋鼠永远追不上右边的袋鼠。最后,如果我们找到一个正的t解,我们应该检查它是否也是一个整数;这可以通过将t转换为整数数据类型并检查其是否等于原始值来实现。下面的示例代码片段实现了这个解决方案。
string kangaroo(int x1, int v1, int x2, int v2) {
if((v2 == v1) && (x1 != x2)) return "NO";
float t = 1.*(x1 - x2)/(v2 - v1);
return ((0 < t) && (t == (int) t)) ? "YES" : "NO";
}
解决同一问题可能有多种有效的方法。在找到一种解决方案后,尝试寻找其他解决方案仍然是有意义且值得的;每种方法都有其优缺点,使得它更适合或不适合特定的问题上下文。为了说明这一点,我们将在下面以不同程度的细节看三个问题。
首先,考虑电影中的美丽日子问题(链接)。阅读问题描述后,可以明显看出,解决问题的关键部分是编写一个函数来反转一个正整数。例如,123 的反转是 321,12000 的反转是 21(注意反转后的数字没有前导零)。
一种解决方法(称为reverse_num_v1)使用除法和取余操作的组合,将最右边的数字移动到最左边的位置,从而自然处理前导零问题;请参见下面的示例实现。这个方法的吸引力在于,由于数字的位数是相对于数字大小对数增长的,reverse_num_v1的时间复杂度是子线性的;空间复杂度也可以忽略不计。
int reverse_num_v1(int x) {
long long res = 0;
while (x) {
res = res * 10 + x % 10;
x /= 10;
// Check for integer overflow
if (res > INT_MAX || res < INT_MIN) return 0;
}
return res;
}
另一种方法(称为reverse_num_v2)使用将整数转换为字符串数据类型、反转字符串、去掉前导零、将字符串重新转换为整数并返回结果的思路;请参见下面的示例实现。
int reverse_num_v2(int x) {
string str = to_string(x);
reverse(str.begin(), str.end());
// Remove leading zeros
str.erase(0, min(str.find_first_not_of('0'), str.size()-1));
int res = stoi(str);
// Check for integer overflow
return (res > INT_MAX || res < INT_MIN) ? 0 : res;
}
这种类型转换在许多编程语言中都是常见做法(如 C++、Python 等),字符串反转和去掉前导零的库函数也可能随时可用,而将多个函数链式调用以形成数据转换操作流水线,是数据科学项目中的典型模式;因此,reverse_num_v2可能是许多数据科学家首先想到的方法。然而,如果内存空间紧张,reverse_num_v1可能是更好的选择,因为整数的字符串表示形式会占用比整数本身更多的空间(请参阅C++中不同数据类型的内存要求文档)。
接下来,让我们简要考虑两个问题,时间转换(link)和魔方阵的构造(link)。虽然这些问题在表面上看起来非常不同,但相同的技术——即使用查找表(或映射表)——可以用来解决这两个问题。在时间转换的情况下,可以使用查找表在 12 小时制和 24 小时制之间为下午时间(例如,下午 8 点映射到 20 点,下午 9 点映射到 21 点,以此类推)提供即时映射。在魔方阵的构造问题中,问题被限制为由 3 行 3 列组成的魔方阵,而恰好有 8 个这样的魔方阵。通过将这 8 个魔方阵的配置存储在查找表中,我们可以实现一个相对简单的解决方案,尽管在HackerRank上它的“中等”难度评级。读者可以通过上面提供的链接更详细地阅读这些问题,但每个解决方案的相关示例代码片段已在下方显示,供比较使用。
时间转换:
string timeConversion(string s) {
// substr(pos, len) starts at position pos and spans len characters
if(s.substr(s.size() - 2) == "AM") {
if(s.substr(0, 2) == "12") return "00" + s.substr(2, s.size() - 4);
else return s.substr(0, s.size() - 2);
}
else {
// PM means add 12 to hours between 01 and 11
// Store all 11 mappings of afternoon hours in a lookup table/map
map<string, string> m = {
{"01", "13"}, {"02", "14"}, {"03", "15"}, {"04", "16"},
{"05", "17"}, {"06", "18"}, {"07", "19"}, {"08", "20"},
{"09", "21"}, {"10", "22"}, {"11", "23"}
};
string hh = s.substr(0, 2);
if(m.count(hh)) return m[s.substr(0, 2)] + s.substr(2, s.size() - 4);
else return s.substr(0, s.size() - 2);
}
}
魔方阵的构造:
请注意,尽管下面的部分代码使用了 3 个嵌套的 for 循环,但实际上只需要 72 次简单操作的循环(833=72)来解决这个问题。
int formingMagicSquare(vector<vector<int>> s) {
// Store all 8 possible 3x3 magic squares in a lookup table/matrix
vector<vector<int>> magic_squares = {
{8, 1, 6, 3, 5, 7, 4, 9, 2},
{6, 1, 8, 7, 5, 3, 2, 9, 4},
{4, 9, 2, 3, 5, 7, 8, 1, 6},
{2, 9, 4, 7, 5, 3, 6, 1, 8},
{8, 3, 4, 1, 5, 9, 6, 7, 2},
{4, 3, 8, 9, 5, 1, 2, 7, 6},
{6, 7, 2, 1, 5, 9, 8, 3, 4},
{2, 7, 6, 9, 5, 1, 4, 3, 8},
};
int min_cost = 81; // Initialize with maximum possible cost of 9*9=81
for (auto& magic_square : magic_squares) {
int cost = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
cost += abs(s[i][j] - magic_square[3*i + j]);
}
}
min_cost = min(min_cost, cost);
}
return min_cost;
}
当一个问题看起来太大或太复杂,无法一次性解决时,将原问题分解成更小的子问题通常是个好主意,这些子问题可以更容易地被解决。这些子问题的具体性质(例如,排序、搜索、转换),以及它们与原问题之间的“部分与整体”关系可能会有所不同。例如,在数据清理的情况下,这是数据科学中的常见问题,每个子问题可能代表数据清理过程中的一个特定的、顺序的步骤(例如,去除停用词、词形还原)。在一个“继续/停止”决策问题中,每个子问题可能反映了必须满足的更小的决策,只有当所有这些决策都为“继续”时,原问题才能解决为“继续”;用逻辑术语来说,可以将其看作是一个复杂的布尔语句,如A AND B。
为了看看分治法如何在实践中运作,我们将考察两个表面上看似非常不同的问题。首先,让我们考虑电子商店问题(link),它本质上是关于约束优化的问题。给定一个总支出预算b和未排序的电脑键盘和 USB 驱动器价格列表(分别称为K和D),目标是购买最昂贵的键盘和驱动器,而不超过预算。在HackerRank上发布的问题中,价格列表最多可以有 1000 个项目,但我们可以想象在实际中这些列表会更长。
一个简单的方法可能是用两个嵌套循环遍历价格列表K和D,找出第i个键盘和第j个硬盘,使预算得到最大化利用。这种方法实现起来很简单,但如果K和D很长,尤其是因为价格列表是未排序的,它会变得非常慢。事实上,简单方法的时间复杂度是二次方的,这对于大数据集的扩展并不乐观。一种更高效的方法是按如下方式进行。首先,排序两个价格列表。其次,选择较短的价格列表进行循环。第三,对于循环列表中的每个项目x,在另一个列表上进行二分查找,找到一个项目y(如果有的话),使得x + y不超过给定的预算b,并将这个结果保存在一个名为max_spent的变量中,变量在循环外部维护。在每次循环迭代中,只有当最新的键盘-硬盘组合的总成本在预算内且超过当前的max_spent值时,max_spent才会更新。
尽管在这个问题中无法避免同时搜索两个价格列表,但高效的方法通过选择较小的价格列表进行循环,并且关键的是,对较长的价格列表进行二分查找(执行时需要对数时间/子线性时间),大大减少了整体搜索时间。此外,虽然最初看起来预先排序两个价格列表会增加解决方案的复杂度,但排序实际上可以非常高效地完成(例如,使用归并排序),并且关键是,这使得可以对较长的价格列表进行二分查找。最终结果是,相比于朴素的方法,这种算法要快得多。下面是高效方法的示例实现:
int findLargestY(int x, int b, const vector<int>& v) {
// Simple implementation of binary search
int i = 0, j = v.size(), y = -1, m, y_curr;
while (i < j) {
m = (i + j) / 2;
y_curr = v[m];
if (x + y_curr <= b) {
y = y_curr;
i = m + 1;
}
else j = m;
}
return y;
}
int getMoneySpent(vector<int> keyboards, vector<int> drives, int b) {
int max_spent = -1;
sort(keyboards.begin(), keyboards.end());
sort(drives.begin(), drives.end());
// Use smaller vector for looping, larger vector for binary search
vector<int> *v1, *v2;
if(keyboards.size() < drives.size()) {
v1 = &keyboards;
v2 = &drives;
}
else {
v1 = &drives;
v2 = &keyboards;
}
int i = 0, j = v2->size(), x, y;
for(int i = 0; i < v1->size(); i++) {
x = (*v1)[i];
if(x < b) {
y = findLargestY(x, b, *v2); // Use binary search
if(y != -1) max_spent = max(max_spent, x + y);
}
else break;
}
return max_spent;
}
接下来,我们来考虑攀登排行榜问题(链接)。假设你正在玩一款街机游戏,并希望在每次尝试后跟踪你在排行榜上的排名。排行榜使用密集排名,所以得分相同的玩家会获得相同的排名。例如,如果得分为 100、90、90 和 80,那么得分 100 的玩家排名第 1,得分 90 的两个玩家排名第 2,得分 80 的玩家排名第 3。排行榜以一个整数数组或列表表示(每个玩家的最高得分),按降序排列。问题的难点在于,每当有新得分加入排行榜时,确定最终的排名并非易事,因为该排名可能会被多个玩家共享。有关详细示例,请参见上述链接中的HackerRank问题描述页面。
尽管电子商店和攀登排行榜问题在HackerRank上的难度评级分别为“简单”和“中等”,但后者从某种程度上更简单,因为排行榜已经是排序好的。下面的示例实现通过在已排序的排行榜上进行二分查找来获取每个新得分后的排名,利用了这一事实:
int find_rank(int x, vector<int>& v) {
// Binary search of rank
int i = 0, j = v.size(), m_pos, m_val;
while(i < j) {
m_pos = (i + j)/2;
m_val = v[m_pos];
if(x == m_val) return m_pos + 1; // Return rank
else if(m_val > x) i = m_pos + 1; // Rank must be lower
else j = m_pos; // Rank must be higher since val < x
}
if(j < 0) return 1; // Top rank
else if(i >= v.size()) return v.size() + 1; // Bottom rank
else return (x >= m_val) ? m_pos + 1 : m_pos + 2; // Some middle rank
}
vector<int> climbingLeaderboard(vector<int> ranked, vector<int> player) {
// Derive vector v of unique values in ranked vector
vector<int> v;
v.push_back(ranked[0]);
for(int i = 1; i < ranked.size(); i++)
if(ranked[i - 1] != ranked[i]) v.push_back(ranked[i]);
// Binary search of rank in v for each score
vector<int> res;
for(auto x : player) res.push_back(find_rank(x, v));
return res;
}
上述讨论的问题提供了算法思维的初步体验,但还有许多其他相关主题值得更深入研究。丹尼尔·津加罗(Daniel Zingaro)所著的书籍《算法思维:基于问题的入门》是继续你学习之旅的绝佳选择 (link)。津加罗具有引人入胜的写作风格,带领读者了解诸如哈希表、递归、动态规划、图搜索等基本概念。书中还包含了关于大 O 符号的附录部分,它是一种表达和推理算法复杂度的便捷方法。另一本以易于理解的方式讲解几种基本算法的书是阿迪提亚·巴尔伽瓦(Aditya Bhargava)所著的《Grokking Algorithms》 (link)。这本书包含了若干有用的插图和 Python 代码片段,是在技术面试之前复习算法思维基础的好资源。
在动态规划方面,由安德烈·格列霍夫(Andrey Grehov)创作的一系列 YouTube 视频 (link to playlist) 提供了一个很好的入门介绍。动态规划是一个非常强大的工具,一旦学会,你会发现有很多机会将其应用于数据科学项目中,例如解决优化问题(其中某些数量如成本或收入必须分别最小化或最大化)或组合问题(其焦点是计数某些事物,实质上是在回答“有多少种方法可以做 XYZ?”)。动态规划可以有效应用于具备以下两个特性的题目:(1)最优子结构,即优化地解决问题的较小部分有助于解决更大的问题;(2)重叠子问题,即在解决一个子问题的过程中,已经计算出的结果可以在不重新计算的情况下用于解决另一个子问题(例如,使用记忆化或缓存)。
最后,本文作者出版的博士论文《网络分析在营销科学中的高级应用》(链接)讨论了应用图论概念解决营销和创新管理中的一系列实际数据科学用例,诸如识别用于新产品开发的有前景的众包创意、动态定价、以及通过匿名化跟踪数据预测客户行为等。该论文展示了如何将表格数据或非结构化数据转化为由节点(实体)和边(实体间关系)组成的图/网络表示,这一过程可以揭示有价值的见解,并推动在广泛的数据科学问题中开发强大的预测模型。
2023 年 10 月,前谷歌计算机科学教授兼工程总监马特·沃尔什在哈佛大学进行了一场引人入胜的讲座(YouTube 链接)。他的演讲标题颇具挑衅性(大语言模型与编程的终结),并提出生成式人工智能——尤其是大语言模型的进展——可能会极大地改变我们开发软件的方式。他指出,人类可能仍然需要在一些角色中发挥作用,比如产品管理(定义软件应该做什么,以及为什么这么做)和软件测试/质量保证(确保软件按预期运行),但他认为,将问题规格转化为生产就绪的代码这一过程,可能在不久的将来通过人工智能实现大规模自动化。到 2023 年底,像 GitHub Copilot 这样的 AI 驱动工具已经显示出自动完成各种基本代码类型(例如,测试用例、简单的循环和条件语句)的能力,并表明它们有潜力提高开发人员的生产力——甚至有可能完全取代开发人员的需求。自那时以来,AI 在提供越来越精确的多模态预测方面取得了令人印象深刻的进展。
在这种背景下,考虑到本文的主题,值得思考的是,在 AI 辅助软件开发的时代,算法思维对数据科学家来说将保持多大程度的相关性。简短的答案是,算法思维可能比以往任何时候都更为重要。更长的答案首先需要承认,即使在今天,许多情况下也可以使用像 ChatGPT 或 GitHub Copilot 这样的生成性 AI 工具生成一个算法草稿版本(如上文所示的代码片段)。毕竟,这些 AI 工具是通过抓取互联网内容进行训练的,互联网上有大量的代码——但这些代码不一定是高质量的,可能会导致“垃圾进,垃圾出”的情况。因此,AI 生成的代码应该始终在用于任何数据科学项目之前进行彻底审查,这意味着仍然需要具备相关技术技能的人类审查员。
此外,AI 生成的代码可能需要定制和/或优化以适应特定的用例,单靠提示工程可能不足够。事实上,设计一个能够可靠生成所需代码的提示(捕捉提示工程师的隐性知识和动机)通常比直接用目标语言编写代码更冗长且耗时,而且无论哪种方法,都不能避免需要正确框定问题并制定合理的实施计划。像框定问题、规划、定制、优化和审查 AI 生成代码以适应特定用例等任务,可能仍然需要一定程度的算法思维,并且需要对代码背后的意图(即“为什么”)有深入的理解。在实践中,似乎不太可能很快将这类工作完全委托给 AI“副驾驶”——这不仅仅是因为涉及到伦理和法律问题;例如,想象一下,如果没有足够的人工监督,让自动驾驶汽车系统的物体避让软件由 AI 生成会是什么样的情形。
算法思维可以帮助数据科学家编写快速且高效利用计算资源(如内存和存储)的代码。随着越来越多背景多样且缺乏足够算法思维训练的数据科学家进入这一领域,本文旨在填补这一知识空白。通过提供高层次的介绍和一些在技术面试中常见的实际示例,本文邀请读者迈出下一步,借助各种教育资源进一步学习算法思维。最终,算法思维是当今数据科学家必备的重要技能,未来在 AI 辅助的时代中依然将是一个值得掌握的技能。