TowardsDataScience-2023-博客中文翻译-四十-

TowardsDataScience 2023 博客中文翻译(四十)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

模拟 105:双摆模型的数值积分

原文:towardsdatascience.com/simulation-105-double-pendulum-modeling-with-numerical-integration-53189ae63959?source=collection_archive---------5-----------------------#2023-08-14

模拟一个混沌系统

Le NguyenTowards Data Science Le Nguyen

·

关注 发布于 Towards Data Science ·9 分钟阅读·2023 年 8 月 14 日

--

摆是一个我们都非常熟悉的经典物理系统。无论是座钟还是秋千上的孩子,我们都见过摆的规律性、周期性运动。单摆在经典物理中定义明确,但双摆(一个摆挂在另一个摆的末端)却是真正的混沌。在这篇文章中,我们将基于对摆的直观理解,建立双摆的混沌模型。物理学非常有趣,而所需的数值方法则是每个人工具箱中的重要工具。

图 1: 混沌双摆的示例

在本文中我们将:

  • 了解谐振动并模拟单摆的行为

  • 学习混沌理论的基础知识

  • 数值建模双摆的混沌行为

谐振动与单摆

简单谐振动

我们将摆的周期性振荡运动描述为谐振动。谐振动发生在系统中存在的运动被与该运动方向相反的比例恢复力平衡时。我们可以在图 2 中看到一个例子,其中一个质量因重力而被拉向下方,但这将能量传递给弹簧,弹簧随后反弹并将质量拉回。弹簧系统旁边,我们看到质量在一个称为相量图的圆周上运动,这进一步说明了系统的规则运动。

图 2: 弹簧上质量的简单谐振动示例

谐振动可以是阻尼的(由于拖拽力而振幅减小)或驱动的(由于外部力的添加而振幅增加),但我们将从最简单的情况——没有外部力作用的无限谐振动(无阻尼运动)开始。这种运动对建模在小角度/低振幅下摆动的单摆是一个很好的近似。在这种情况下,我们可以使用下面的方程 1 来模拟运动。

方程 1: 小角度摆的简单谐振动

我们可以轻松地将这个函数编入代码,并模拟一个简单的摆动随时间变化的过程。

def simple_pendulum(theta_0, omega, t, phi):
    theta = theta_0*np.cos(omega*t + phi)
    return theta

#parameters of our system
theta_0 = np.radians(15) #degrees to radians

g = 9.8 #m/s²
l = 1.0 #m
omega = np.sqrt(g/l)

phi = 0 #for small angle

time_span = np.linspace(0,20,300) #simulate for 20s split into 300 time intervals
theta = []
for t in time_span:
    theta.append(simple_pendulum(theta_0, omega, t, phi))

#Convert back to cartesian coordinates
x = l*np.sin(theta)
y = -l*np.cos(theta) #negative to make sure the pendulum is facing down

图 3: 简单摆模拟

使用拉格朗日力学分析完整的摆动运动

一个简单的小角度摆动是一个良好的起点,但我们希望超越这一点并模拟完整摆动的运动。由于我们不能再使用小角度近似,因此最好使用拉格朗日力学来建模摆动。这是物理学中的一个基本工具,它将我们从观察系统中的力转变为观察系统中的能量。我们将参考框架从驱动力与恢复力切换为动能势能

拉格朗日量是方程 2 中给出的动能与势能之差。

方程 2: 拉格朗日量

将方程 3 中给出的摆的动能和势能代入,得到的是方程 4 中显示的摆的拉格朗日量

方程 3:摆的动能和势能

方程 4:摆的拉格朗日量

有了摆的拉格朗日量,我们现在描述了系统的能量。还有最后一步数学运算,将其转化为可以构建仿真的内容。我们需要通过欧拉-拉格朗日方程从能量参考桥接回动态/力导向的参考。使用这个方程,我们可以用拉格朗日量来得到摆的角加速度。

方程 5:欧拉-拉格朗日方程中的角加速度

在进行数学运算后,我们得到了角加速度,可以用来得到角速度和角度本身。这将需要一些数值积分,这些内容将在我们的完整摆仿真中详细说明。即使是单摆,非线性动力学也意味着没有解析解来求解theta,因此需要数值解。积分过程相当简单(但强大),我们使用角加速度更新角速度,再使用角速度更新theta,通过将前者加到后者上并乘以一个时间步长。这样可以得到加速度/速度曲线下的面积的近似值。时间步长越小,近似值越准确。

def full_pendulum(g,l,theta,theta_velocity, time_step):
    #Numerical Integration
    theta_acceleration = -(g/l)*np.sin(theta) #Get acceleration
    theta_velocity += time_step*theta_acceleration #Update velocity with acceleration
    theta += time_step*theta_velocity #Update angle with angular velocity
    return theta, theta_velocity

g = 9.8 #m/s²
l = 1.0 #m

theta = [np.radians(90)] #theta_0
theta_velocity = 0 #Start with 0 velocity
time_step = 20/300 #Define a time step

time_span = np.linspace(0,20,300) #simulate for 20s split into 300 time intervals
for t in time_span:
    theta_new, theta_velocity = full_pendulum(g,l,theta[-1], theta_velocity, time_step)
    theta.append(theta_new)

#Convert back to cartesian coordinates 
x = l*np.sin(theta)
y = -l*np.cos(theta)

图 4:完整摆的仿真

我们已经模拟了一个完整的摆,但这仍然是一个定义明确的系统。现在是时候进入双摆的混沌领域了。

混沌与双摆

混沌在数学意义上指的是对初始条件高度敏感的系统。系统开始时的微小变化会导致系统演变过程中行为的巨大差异。这完美地描述了双摆的运动。与单摆不同,双摆不是一个表现良好的系统,甚至初始角度的微小变化都会导致它以截然不同的方式演变。

为了模拟双摆的运动,我们将使用与之前相同的拉格朗日方法(查看完整推导)。

我们在将这个方程实现到代码中并找到 theta 时,将继续使用之前相同的数值积分方案。

#Get theta1 acceleration 
def theta1_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g):
    mass1 = -g*(2*m1 + m2)*np.sin(theta1)
    mass2 = -m2*g*np.sin(theta1 - 2*theta2)
    interaction = -2*np.sin(theta1 - theta2)*m2*np.cos(theta2_velocity**2*l2 + theta1_velocity**2*l1*np.cos(theta1 - theta2))
    normalization = l1*(2*m1 + m2 - m2*np.cos(2*theta1 - 2*theta2))

    theta1_ddot = (mass1 + mass2 + interaction)/normalization

    return theta1_ddot

#Get theta2 acceleration
def theta2_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g):
    system = 2*np.sin(theta1 - theta2)*(theta1_velocity**2*l1*(m1 + m2) + g*(m1 + m2)*np.cos(theta1) + theta2_velocity**2*l2*m2*np.cos(theta1 - theta2))
    normalization = l1*(2*m1 + m2 - m2*np.cos(2*theta1 - 2*theta2))

    theta2_ddot = system/normalization
    return theta2_ddot

#Update theta1
def theta1_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step):
    #Numerical Integration
    theta1_velocity += time_step*theta1_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g)
    theta1 += time_step*theta1_velocity
    return theta1, theta1_velocity

#Update theta2
def theta2_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step):
    #Numerical Integration
    theta2_velocity += time_step*theta2_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g)
    theta2 += time_step*theta2_velocity
    return theta2, theta2_velocity

#Run full double pendulum
def double_pendulum(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step,time_span):
    theta1_list = [theta1]
    theta2_list = [theta2]

    for t in time_span:
        theta1, theta1_velocity = theta1_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step)
        theta2, theta2_velocity = theta2_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step)

        theta1_list.append(theta1)
        theta2_list.append(theta2)

    x1 = l1*np.sin(theta1_list) #Pendulum 1 x
    y1 = -l1*np.cos(theta1_list) #Pendulum 1 y

    x2 = l1*np.sin(theta1_list) + l2*np.sin(theta2_list) #Pendulum 2 x
    y2 = -l1*np.cos(theta1_list) - l2*np.cos(theta2_list) #Pendulum 2 y

    return x1,y1,x2,y2
#Define system parameters
g = 9.8 #m/s²

m1 = 1 #kg
m2 = 1 #kg

l1 = 1 #m
l2 = 1 #m

theta1 = np.radians(90)
theta2 = np.radians(45)

theta1_velocity = 0 #m/s
theta2_velocity = 0 #m/s

theta1_list = [theta1]
theta2_list = [theta2]

time_step = 20/300

time_span = np.linspace(0,20,300)
x1,y1,x2,y2 = double_pendulum(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step,time_span)

图 5:双摆仿真

我们终于成功了!我们成功地建模了一个双摆,但现在是观察一些混沌的时候了。我们的最终模拟将是两个双摆,起始条件略有不同。我们将设置一个摆的theta 1为 90 度,另一个为 91 度。让我们看看会发生什么。

图 6:两个起始条件略有不同的双摆

我们可以看到,两个摆开始时的轨迹类似,但很快就发生了分歧。这就是我们所说的混沌,即使是 1 度的角度差异也会导致截然不同的最终行为。

结论

在这篇文章中,我们了解了摆的运动及其建模方法。我们从最简单的谐振动模型开始,逐步建立到复杂和混沌的双摆。在这个过程中,我们学习了拉格朗日量、混沌和数值积分。

双摆是混沌系统的最简单例子。这些系统在我们的世界中随处可见,从种群动态气候台球。我们可以将从双摆中学到的经验应用到遇到的任何混沌系统中。

关键要点

  • 混沌系统对初始条件非常敏感,即使是对起始条件的微小变化,也会以截然不同的方式演变。

  • 在处理一个系统,特别是混沌系统时,是否有其他参考框架可以使其更易于处理?(类似于力参考系转到能量参考系)

  • 当系统变得过于复杂时,我们需要实施数值解法来解决它们。这些解法简单但强大,能够提供对实际行为的良好近似。

参考文献

本文使用的所有图形要么由作者创建,要么来自数学图像,并且完全遵循GNU 自由文档许可证 1.2

[## 一切—是的,一切—都是一个谐振子

物理学本科生可能会开玩笑说宇宙是由谐振子组成的,但他们并没有说得离谱。

www.wired.com](https://www.wired.com/2016/07/everything-harmonic-oscillator/?source=post_page-----53189ae63959--------------------------------)

经典力学,约翰·泰勒 neuroself.files.wordpress.com/2020/09/taylor-2005-classical-mechanics.pdf

完整代码

简单摆

def makeGif(x,y,name):
    !mkdir frames

    counter=0
    images = []
    for i in range(0,len(x)):
        plt.figure(figsize = (6,6))

        plt.plot([0,x[i]],[0,y[i]], "o-", color = "b", markersize = 7, linewidth=.7 )
        plt.title("Pendulum")
        plt.xlim(-1.1,1.1)
        plt.ylim(-1.1,1.1)
        plt.savefig("frames/" + str(counter)+ ".png")
        images.append(imageio.imread("frames/" + str(counter)+ ".png"))
        counter += 1
        plt.close()

    imageio.mimsave(name, images)

    !rm -r frames

def simple_pendulum(theta_0, omega, t, phi):
    theta = theta_0*np.cos(omega*t + phi)
    return theta

#parameters of our system
theta_0 = np.radians(15) #degrees to radians

g = 9.8 #m/s²
l = 1.0 #m
omega = np.sqrt(g/l)

phi = 0 #for small angle

time_span = np.linspace(0,20,300) #simulate for 20s split into 300 time intervals
theta = []
for t in time_span:
    theta.append(simple_pendulum(theta_0, omega, t, phi))

x = l*np.sin(theta)
y = -l*np.cos(theta) #negative to make sure the pendulum is facing down

def full_pendulum(g,l,theta,theta_velocity, time_step):
    theta_acceleration = -(g/l)*np.sin(theta)
    theta_velocity += time_step*theta_acceleration
    theta += time_step*theta_velocity
    return theta, theta_velocity

g = 9.8 #m/s²
l = 1.0 #m

theta = [np.radians(90)] #theta_0
theta_velocity = 0
time_step = 20/300

time_span = np.linspace(0,20,300) #simulate for 20s split into 300 time intervals
for t in time_span:
    theta_new, theta_velocity = full_pendulum(g,l,theta[-1], theta_velocity, time_step)
    theta.append(theta_new)

#Convert back to cartesian coordinates 
x = l*np.sin(theta)
y = -l*np.cos(theta)

#Use same function from simple pendulum
makeGif(x,y,"pendulum.gif")

双摆

def theta1_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g):
    mass1 = -g*(2*m1 + m2)*np.sin(theta1)
    mass2 = -m2*g*np.sin(theta1 - 2*theta2)
    interaction = -2*np.sin(theta1 - theta2)*m2*np.cos(theta2_velocity**2*l2 + theta1_velocity**2*l1*np.cos(theta1 - theta2))
    normalization = l1*(2*m1 + m2 - m2*np.cos(2*theta1 - 2*theta2))

    theta1_ddot = (mass1 + mass2 + interaction)/normalization

    return theta1_ddot

def theta2_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g):
    system = 2*np.sin(theta1 - theta2)*(theta1_velocity**2*l1*(m1 + m2) + g*(m1 + m2)*np.cos(theta1) + theta2_velocity**2*l2*m2*np.cos(theta1 - theta2))
    normalization = l1*(2*m1 + m2 - m2*np.cos(2*theta1 - 2*theta2))

    theta2_ddot = system/normalization
    return theta2_ddot

def theta1_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step):

    theta1_velocity += time_step*theta1_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g)
    theta1 += time_step*theta1_velocity
    return theta1, theta1_velocity

def theta2_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step):

    theta2_velocity += time_step*theta2_acceleration(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g)
    theta2 += time_step*theta2_velocity
    return theta2, theta2_velocity

def double_pendulum(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step,time_span):
    theta1_list = [theta1]
    theta2_list = [theta2]

    for t in time_span:
        theta1, theta1_velocity = theta1_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step)
        theta2, theta2_velocity = theta2_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step)

        theta1_list.append(theta1)
        theta2_list.append(theta2)

    x1 = l1*np.sin(theta1_list)
    y1 = -l1*np.cos(theta1_list)

    x2 = l1*np.sin(theta1_list) + l2*np.sin(theta2_list)
    y2 = -l1*np.cos(theta1_list) - l2*np.cos(theta2_list)

    return x1,y1,x2,y2
#Define system parameters, run double pendulum
g = 9.8 #m/s²

m1 = 1 #kg
m2 = 1 #kg

l1 = 1 #m
l2 = 1 #m

theta1 = np.radians(90)
theta2 = np.radians(45)

theta1_velocity = 0 #m/s
theta2_velocity = 0 #m/s

theta1_list = [theta1]
theta2_list = [theta2]

time_step = 20/300

time_span = np.linspace(0,20,300)
for t in time_span:
    theta1, theta1_velocity = theta1_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step)
    theta2, theta2_velocity = theta2_update(m1,m2,l1,l2,theta1,theta2,theta1_velocity,theta2_velocity,g,time_step)

    theta1_list.append(theta1)
    theta2_list.append(theta2)

x1 = l1*np.sin(theta1_list)
y1 = -l1*np.cos(theta1_list)

x2 = l1*np.sin(theta1_list) + l2*np.sin(theta2_list)
y2 = -l1*np.cos(theta1_list) - l2*np.cos(theta2_list)
#Make Gif
!mkdir frames

counter=0
images = []
for i in range(0,len(x1)):
    plt.figure(figsize = (6,6))

    plt.figure(figsize = (6,6))
    plt.plot([0,x1[i]],[0,y1[i]], "o-", color = "b", markersize = 7, linewidth=.7 )
    plt.plot([x1[i],x2[i]],[y1[i],y2[i]], "o-", color = "b", markersize = 7, linewidth=.7 )
    plt.title("Double Pendulum")
    plt.xlim(-2.1,2.1)
    plt.ylim(-2.1,2.1)
    plt.savefig("frames/" + str(counter)+ ".png")
    images.append(imageio.imread("frames/" + str(counter)+ ".png"))
    counter += 1
    plt.close()

imageio.mimsave("double_pendulum.gif", images)

!rm -r frames

模拟 106:使用网络建模信息扩散和社会传染

原文:towardsdatascience.com/simulation-106-modeling-information-diffusion-and-social-contagion-with-networks-7c1184004889

基于图的方法来建模信息通过社交网络的传播

Le NguyenTowards Data Science Le Nguyen

·发表于 Towards Data Science ·12 分钟阅读·2023 年 9 月 7 日

--

社交媒体彻底改变了信息的格局。我们比人类历史上任何时候都更加互联。新闻可以瞬间传达给我们,思想在几天内传播到全球,但这如何运作?信息是如何传播的,我们能否对其进行建模?在本文中,我们将介绍信息传播的理论,并使用网络进行建模。

图 1: 通过网络连接的世界的艺术表现。根据 知识共享 许可。

在本文中,我们将:

  • 学习图和网络理论的基础

  • 概述信息扩散和社会传染

  • 构建一个计算框架来模拟思想的传播

图和网络

什么是图和网络?

是一种数学结构,用于显示对象之间的关系。它通过将每个对象表示为一个顶点,并用将其连接到其他顶点,从而展示它们之间的关系。

图 2: 具有 7 条边和 6 个顶点的图示例

有许多图的类型,包括加权图,在这些图中,所有边都被赋予值,以不仅显示关系,还显示关系的强度,以及有向图,在这些图中,边有一个方向以显示有向关系。各种类型的图使它们成为展示实体之间关系的强大工具,我们将讨论这些工具。

从概念上讲,图和网络是相同的,在大多数情况下,这些术语可以互换使用。然而,根据惯例, 用于指代抽象的数学概念,而网络 用于实际应用的上下文。例如,我们使用了“计算机网络”和“社交网络”这两个术语,即使计算机图或社交图也可以表达相同的意思。对于我们的目的,我们将说网络是应用图,唯一需要的术语差异是将顶点 叫作节点

图 3: 具有指定特征的网络示例

图论

图论.) 是研究图(和网络)的数学分支,描述图的性质以及它们的应用。我们不会在这篇文章中覆盖图论的整个领域,但我们将关注与信息传播相关的要点。

信息的传播可以用流图建模,或者在我们的例子中是流网络。流网络是一种加权的有向图,它展示了在结构中某物的转移或运输。这可能是管道中的水、计算机网络中的数据,或者在我们的例子中是社交网络中的信息。

流网络具有任何网络的所有组件(节点、边、权重),但现在它们是某种结构中流动的特定表示。在我们的例子中,节点 是社交网络中的个体,这些个体通过 连接到其他个体,而权重 则由它们之间的联系程度决定。流网络还可以建立在一个 上,即信息的起点,以及一个,即信息流终止的地方,还有一个容量 或者说网络中可以传输的最大信息量。

图 4:社会流动网络示例

图 4 是一个表示教室的社会流动网络。每个学生(以及老师)是这个网络中的一个节点,通过有向边与教室中的其他节点相连,显示他们之间的联系。在这种情况下,边没有权重,但节点的大小由其中心性决定。

图表/网络中心性的度量有很多种,但总体中心性反映了一个节点在网络中的重要性。一些例子包括度中心性,即连接到一个节点的边的数量,以及接近中心性,即一个节点到所有其他节点的短路径数量。在图 4 中,我们看到节点的大小由度中心性决定,老师(SL)是网络中最中心的节点,具有最高的度中心性。

通过对图形和网络的基本理解,我们可以深入探讨下一部分内容;信息扩散和社会传染。

信息扩散与社会传染

信息扩散指的是信息在群体或系统中的传播。对于一个群体,信息通过成员以各种方式传播,如面对面、在线、书面、通过言语或录音等。信息扩散用于描述谣言的传播、广告活动的有效性,以及信息在群体中的其他传播方式。

我们不能谈论信息扩散而不讨论社会传染。它研究信息在信息扩散过程中如何通过群体传播及其影响因素。社会传染因素包括社会联系、媒体覆盖和文化规范等,以研究信息传播的速度或传染性、信息传播到的群体范围,以及群体成员对新信息的抵抗。社会传染可用于研究现象,如病毒视频的快速传播、阴谋论的传播以及新趋势的采纳。

图 5:信息扩散和社会传染的插图。我们可以看到信息从一个人开始传播,扩散到更大的群体中。图中的箭头表示信息的传染过程。

扩散和传染模型

信息扩散和社会传播都是经过充分研究的话题,并且有理论模型为它们开发。我们将首先查看一个简单的信息扩散模型,称为 两步流动模型

两步流动模型从某种大规模媒体开始,比如新闻网络或大型公司发布一个想法,然后由意见领袖/社区中高度影响力的成员采纳。随后,社区成员会从他们最亲近的意见领袖那里接受这个想法。这个从媒体到意见领袖再到他们的社区的两步过程假设大多数人不会直接从大众媒体中采纳想法;他们需要一个更接近他们的来源来分享这个想法,才能采纳它。

例如,假设有一款新手机发布,广告随处可见。一般公众可能会对广告感到麻木,或被竞争手机公司的广告淹没,这不会说服他们购买新手机。然而,如果他们从更亲近的来源,如喜欢的名人、网红、同事或家庭成员那里听到这款新手机,那将更具说服力。我们可以在图 6 中看到这种两步流动过程的可视化。

图 6: 信息扩散的两步流动模型

在深入探讨模拟之前,我们将观察一种社会传播模型。社会传播将信息的传播视为一种疾病(因此称为“传播”),这种疾病通过接触信息而传播。因此,基本的社会传播模型借鉴了疾病建模,并且类似于我们之前看到的 SIR 曲线 当我们模拟疾病传播时

这个曲线可以在图 7 中看到,它是对图 5 中所见内容的线性建模。首先,一个想法由个人或小组持有,他们将其传播给他们最亲近的人。然后,这些人将其传播给他们最亲近的人,将信息扩散到人群中。这个迭代过程最初会以指数级传播想法,直到它达到一个临界质量,然后在主流之后线性传播这个想法。一旦覆盖到所有人并且没有更多人可以传播,这种增长将开始减缓。这与通过接触传播的疾病的行为是相同的,因此信息以类似的方式传播是合理的。

图 7: 信息传播曲线

我们现在可以结合对网络和想法传播的理解,创建一个计算框架来模拟信息和想法的传播。

模拟

在本次模拟中,我们将使用类似于我们在基于代理的疾病模型中创建的 python 类方法。我们还将使用NetworkX库来构建网络。我们首先创建一个网络类,并添加一些初始方法。我们需要一个方法来在网络中创建初始信息源,以及添加新节点(人)和有向边(连接)。实现这一点的代码如下,并在图 8 中进行了可视化。

class Network:

    def create_source(self):
        G = nx.DiGraph()
        self.G = G
        self.G.add_node(0)

    def add_node(self):
        index = len(self.G.nodes)
        self.G.add_node(index)

    def add_connection(self,node1,node2):
        self.G.add_edge(node1,node2)

图 8:网络类的初始方法。

现在我们可以初始化信息源,向网络中添加更多的人,并在信息源与网络中的人之间以及人与人之间添加连接。不过,在运行完整的模拟之前,我们需要在网络类中添加更多功能。我们需要一种方法来跟踪网络中初始想法已经到达的人,并拥有传播该想法的机制。

通过为节点添加特性来完成这一点,这由 NetworkX 的功能轻松处理。我们给图中的每个人或节点添加“informed”特性,这是一个布尔值,告诉我们这个想法是否已经到达他们那里。通过我们的网络传播一个想法将使越来越多的节点的informed特性值设置为 1(true)。

在节点中添加了informed特性后,我们还可以创建一个方法,通过网络传播一个想法。我们通过遍历网络中的边列表(由 NetworkX 对象提供)来实现这一点,查看哪些节点是相互连接的。如果一个已知的节点连接到一个未知的节点(informed特性值为 0 或 false),那么它将把自己的想法传播给未知的节点;将其informed特性值设置为 1。

为了使这次模拟更加现实,我们将传播机制设为概率性的,因为并非所有人与人之间的互动都会传播想法或特定的想法。我们将通过对边值进行加权(赋予节点之间的连接一个强度值),然后随机对这个边值进行滚动,来判断是否在两个节点之间传播想法。为了这次模拟,我们将通过边的度中心性来加权边缘,因此网络中中心度更高、连接更好的节点将有更高的传播概率。

此外,这种随机对边值的滚动将由阻力值决定。阻力值是社会传播理论中出现的一个概念,用于确定一个人接受或传播新思想的抗拒程度。在我们的网络中,个体的阻力值越低,他们越有可能在对边值的滚动中获得积极结果,从而获得信息。

添加了信息特征和传播方法,代码如下所示,并在图 8 中进行了可视化。

class Network:

    def create_source(self):
        G = nx.DiGraph()
        self.G = G
        self.G.add_node(0)
        self.G.nodes[0]["Informed"] = 1

    def add_node(self):
        index = len(self.G.nodes)
        self.G.add_node(index)
        self.G.nodes[index]["Informed"] = 0

    def add_connection(self,node1,node2):
        self.G.add_edge(node1,node2)
        self.G[node1][node2]["Connection"] = nx.degree_centrality(self.G)[node1]

    def propogate_information(self, resistance):
        for edge in list(self.G.edges):
            rand = np.random.uniform(0,resistance)
            if self.G.nodes[edge[0]]["Informed"] == 1 and rand < self.G[edge[0]][edge[1]]["Connection"]:
                self.G.nodes[edge[1]]["Informed"] = 1

图 8: 节点现在以绿色(真)和红色(假)进行二进制着色,以显示其信息特征值。边缘也按度中心性加权。

通过我们构建的方法,我们可以创建一个社交网络。我们的网络以信息源开始,然后我们添加 100 个节点(人)。接下来,我们通过随机选择网络中的 2 个节点并在它们之间创建有向边来添加人与人之间的连接。这一过程重复 300 次,意味着平均每个人与其他 3 个人相连。图 9 显示了在初始社交网络中执行此操作的结果,其中想法被保留在源头。

def get_color(network):
    color_dict = dict({0:"red",1:"green"})
    color = list(dict(network.G.nodes(data="Informed")).values())
    color = [color_dict[i] for i in color]
    return color

network = Network()
network.create_source()

for i in range(0,100):
    network.add_node()

nodes = list(network.G.nodes)
for i in range(0,300):
    if i == 0: 
        node1 = nodes[0]
    else:
        node1 = np.random.choice(nodes)
    node2 = np.random.choice(nodes)

    if node1 != node2:
        network.add_connection(node1,node2)

pos = nx.kamada_kawai_layout(network.G)
color = get_color(network)

plt.figure(figsize =(10,10))
nx.draw(network.G,  node_color = color, arrowsize=20, pos = pos)

图 9: 初始社交网络

现在是时候将我们的想法从源头传播到网络的其余部分了。我们将把传播方法简单地加入一个循环中,让其在给定的时间步数内在网络中传播。这里我们将传播这个想法 50 个时间步,并给每个人一个统一的抵抗值 0.3。

该传播的动画显示在图 10 中,最终网络显示在图 11 中。

informed = []
networks = [network.G.copy()]
for i in range(0,50):
    network.propogate_information(.3)
    informed.append(sum(list(dict(network.G.nodes(data="Informed")).values())))
    networks.append(network.G.copy())

图 10: 想法传播的动画

图 11: 最终网络

通过我们的模拟,我们可以通过保存每个时间步的信息节点数来跟踪想法在网络中的传播情况。这可以在图 12 中看到,它与我们期望的社会传播理论中的理论曲线非常接近,即图 7。我们看到经典的 S 形曲线,其中想法呈指数增长,经过线性阶段,最后在网络被想法饱和时趋于平稳。仔细观察我们的网络,有一些节点没有指向它们的边缘,只是从它们指向其他节点,这意味着它们将信息流入网络,但反向却不是这样。这些节点将永远不会被通知,因此我们的总人口将永远无法完全接触到源想法。

图 12: 传播曲线

结论

在本文中,我们了解了图形和网络作为表示对象之间关系的数学结构。我们还通过讨论信息扩散和社会传播理论来了解了思想的传播。然后,我们结合对网络和思想传播的理解,模拟了一个想法在社交网络中的传播。

完整代码

#Import Libraries
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import imageio

#Make helper functions
def makeGif(networks, name):
    !mkdir frames

    counter=0
    images = []
    for i in range(0,len(networks)):
        plt.figure(figsize = (8,8))

        color = get_color(networks[i])
        nx.draw(networks[i],  node_color = color, arrowsize=20, pos = pos)
        plt.savefig("frames/" + str(counter)+ ".png")
        images.append(imageio.imread("frames/" + str(counter)+ ".png"))
        counter += 1
        plt.close()

    imageio.mimsave(name, images)

    !rm -r frames

def get_color(graph):
    color_dict = dict({0:"red",1:"green"})
    color = list(dict(graph.nodes(data="Informed")).values())
    color = [color_dict[i] for i in color]
    return color

#Create Network Class
class Network:

    def create_source(self):
        G = nx.DiGraph()
        self.G = G
        self.G.add_node(0)
        self.G.nodes[0]["Informed"] = 1

    def add_node(self):
        index = len(self.G.nodes)
        self.G.add_node(index)
        self.G.nodes[index]["Informed"] = 0

    def add_connection(self,node1,node2):
        self.G.add_edge(node1,node2)
        self.G[node1][node2]["Connection"] = nx.degree_centrality(self.G)[node1]

    def propogate_information(self, resistance):
        for edge in list(self.G.edges):
            rand = np.random.uniform(0,resistance)
            if self.G.nodes[edge[0]]["Informed"] == 1 and rand < self.G[edge[0]][edge[1]]["Connection"]:
                self.G.nodes[edge[1]]["Informed"] = 1
#Run simulation
network = Network()
network.create_source()

for i in range(0,100):
    network.add_node()

nodes = list(network.G.nodes)
for i in range(0,300):
    if i == 0: 
        node1 = nodes[0]
    else:
        node1 = np.random.choice(nodes)
    node2 = np.random.choice(nodes)

    if node1 != node2:
        network.add_connection(node1,node2)

pos = nx.kamada_kawai_layout(network.G)
color = get_color(network.G)

#Plot initial netowork
plt.figure(figsize =(10,10))
nx.draw(network.G,  node_color = color, arrowsize=20, pos = pos)

#Propogate idea
informed = []
networks = [network.G.copy()]
for i in range(0,50):
    network.propogate_information(.3)
    informed.append(sum(list(dict(network.G.nodes(data="Informed")).values())))
    networks.append(network.G.copy())

#Plot final network
plt.figure(figsize =(10,10))
color = get_color(network.G)
nx.draw(network.G,  node_color = color, arrowsize=20, pos = pos)

#Plot contagion curve
plt.figure()
t = np.arange(0,len(informed),1)
plt.plot(t,informed)
plt.xlabel("Time")
plt.ylabel("Informed Members")
plt.title("Information Contagion Curve")
plt.savefig("contagionCurve.png")

#Save gif
makeGif(networks, "contagion.gif") 

参考文献

本文中使用的所有图像均由作者创建,取自作者自己的作品,或属于创意共享许可

阮乐,《基于图的激进在线情绪传播研究方法》(2023)。论文。罗彻斯特理工学院。访问于

scholarworks.rit.edu/theses/11453

仿真优化:帮助我的朋友建模和优化他公司的支持台

原文:towardsdatascience.com/simulation-optimization-helping-my-friend-model-and-optimize-his-companys-support-desk-be6ae83b4ae3?source=collection_archive---------3-----------------------#2023-12-07

由 DreamStudio 生成。

一个关于创建仿真优化模型以帮助简化支持台人员配置的故事。

Maxwell WulffTowards Data Science Maxwell Wulff

·

关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 12 月 7 日

--

介绍

一开始这是一个相当简单的请求。我的朋友,负责运营和管理一个支持中心,遇到了一些困难。在任何给定的时间,支持台的员工似乎都没有得到有效优化,要么人手过多,要么人手不足。他有相当干净的数据,记录了电话(电话、聊天消息和电子邮件)何时到来、呼叫者在等待中的时间以及对话持续的时间。知道我在运筹学(OR)方面的背景,我的朋友向我介绍了这个问题。我很兴奋——在我的职业生涯中,我不常使用 OR 原理。这是一个让我重新接触我投入了五年时间研究的领域的机会。

运筹学程序的核心是排队理论,这正是一个* m/m/c* 队列的教科书案例(字面意义上)。电话以设定的随机速率进入,一些员工处理来电,所有电话都以设定的随机速率处理,随机速率应遵循指数分布。所以我启动了 Jupyter Notebook,使用 scipy 将指数分布拟合到一些来电和处理时间上。我发现,确实,我们的参数与指数分布非常匹配。

m/m/c 队列。图表由作者提供。

现在,如果我的朋友仅仅想知道他需要多少名员工来支撑他的支持台,有公式可以准确告诉你这个;主要是Erlang-C。[1] 然而,就像任何现实世界的场景一样,当我们开始引入影响这个特定支持中心的众多参数时,这个公式很快就失效了。举几个例子:全天的来电需求波动、员工效率、休息时间、员工需要轮班,等等。因此,这使我转向了模拟,这是运筹学的另一个核心内容。

模拟

模拟很有用,因为你不受限于特定的公式。你可以利用随机系统的属性,知道任何具有随机输入的系统将会收敛到一个设定的输出——前提是你运行系统足够多次以获得可靠的平均值。系统和数据中的大多数异常情况可以通过调整模拟的部分并将输出与现实世界的训练数据进行匹配来解释。

运行模拟 5 次与 50 次。图表由作者提供。

所以我开始工作。在接下来的几天里,我用 Python 构建了一个支持台的模拟,使用了真实的数据输入。我的模拟的核心部分包括考虑全天的来电时间和代理处理时间的变化,同时还考虑了诸如队列长度和参数变化时工作的代理等组件。现在是决定我们要测量什么的时候了,测量选项有很多。我开始测量代理的利用率和队列的平均长度,但支持中心的主要指标之一是连接时间服务水平协议(SLA)。[2] SLA 本质上是对人们在队列中等待时间的约定目标测量。SLA 的重要性不仅在于作为平均值进行监控,还要作为全天的时间序列进行监控。支持中心希望在短时间内、可预测的时间内接听电话,无论有人何时打电话。

幸运的是,这相对容易。每当我在模拟运行过程中进行测量时,我会检查排队的第一个人等待了多久。知道排队的第一个人等待的时间最长,我们可以知道最大 SLA 仍然得到保持。我们用这些数据创建了训练和验证集,进行了更多的调整,我的朋友有了一个可以用来更改代理人数、来电参数并查看 SLA 变化的工具。不能忽视的是,这部分项目花费了很多时间。

模拟结果与一周滚动平均队列时间的比较。图表由作者提供。

优化

在一次公路旅行中,我们讨论了这个工具虽然很有趣,但不一定实用。我的朋友可以输入数字并查看结果,但这并没有使人员配置决策变得更容易,因为它基本上是经过教育的试错法。更有帮助的是一个系统,在这个系统中你可以设置一个全天的目标 SLA,系统会根据可变的来电参数和代理处理时间给出最佳的代理安排。这是运筹学的第三个重要支柱:优化

问题在于,优化需要一个公式来优化,而我们只有一个模拟。然而,我突然意识到没有理由我们不能将模拟用作优化问题的目标函数。它接受一组输入,通过足够多的模拟运行,给出一个可信的输出。幸运的是,30 年前的研究人员也有过相同的想法,并从那时起一直在研究模拟优化领域。这些聪明的人为我做了所有的艰苦工作。[3, 4]

所以,我再次开始工作。现在,仿真优化的核心是创建一组输入,运行仿真,并以智能的方式选择下一组输入,直到达到目标。我的优化问题看起来像这样:

我的目标是制定一个时间表。时间表由班次组成。班次是在仿真“日”中的固定时间段(比如上午 9 点到下午 5 点,上午 10 点到下午 4 点),在这些时间段内,代理可以工作。时间表是所有班次的集合,包括每个班次上工作的代理人数。一个好的时间表是能让 SLA 保持在目标附近,并最小化总工时(工人数 * 班次长度)的时间表。某些班次有员工数量上限(例如,半天早班最多只能有 10 人),每个班次都有一定的休息时间表。SLA 不应超过目标之上的硬性上限(例如,我们的目标是 3 分钟的 SLA,但没有人应等待超过 10 分钟)。

我设置了框架以考虑所有这些约束条件,现在我准备开始优化。仿真优化研究中讨论的一些问题很困难,因为它们的仿真是黑箱,即除了仿真答案外没有任何有用的信号。幸运的是,这个仿真不同。我们在整个仿真“日”中测量排队时间,因此我们可以看到某些班次的表现如何。

例如,如果早班的员工比晚班多,但晚班的来电更多,我们可能会发现晚班的目标偏差比早班大。在下一次尝试中,我们可能应该增加晚班的人员数量。

事情并不像这般简单——由于复杂的重叠班次和其他参数会有相互依赖性,但我们可以将任何时间表插入仿真中看看会发生什么。

我们从仿真中得到的这些信号使我得出了明确的策略——梯度。

基于梯度的搜索可以理解为一个球滚下山坡,每一步你都试图让球达到一个更低的状态,直到它到达底部。这被一个不均匀的山坡所复杂化。在山坡上会有虚假的底部,你选择的任何方向都会回到山坡上,但真正的底部需要从不同的位置开始滚动。在实际操作中,我们永远无法知道是否到达了真正的底部,但有聪明的方法来确保我们进行足够的测试,以确定我们非常接近。在我们的例子中,球是一个时间表,而山坡的底部是完美的线路,在那里所有的来电等待的时间正好符合目标服务水平协议(SLA)。

由 DreamStudio 生成。

那么,最自然的梯度步骤是什么?取最差的计划(即与目标平均最远的那个),如果平均值高于目标,则在变换中添加一个代理;如果平均值低于目标,则去掉一个代理。如果你陷入了所有变换都使计划变得更差但 SLA 距离目标仍然较远的情况,回到一个以前较好的计划,从那里重新开始。我尝试了其他选择下一个计划的方法,但这种方法最为一致。如何知道何时停止?设定一个容差值,比如说如果每个变换平均距离目标少于 30 秒,则认为问题已解决。如果步数过多则停止,并从模拟出的计划中选择最佳方案。

选择下一个计划。图示由作者提供。

在理想的世界里,我们会永远运行这个程序,尝试每一个计划,直到找到最佳方案,但我们需要这个程序在 5 分钟内选择一个计划。这个项目中最有趣的部分是测试优化算法,以找到能最快得到良好解决方案的算法。

结果最好的解决方案利用了你可以控制模拟运行次数的事实,权衡是运行次数越少,对结果的信心越低。我发现以下方法有效:

开始时运行优化算法时,先使用较少的模拟次数,随着目标接近,逐渐增加模拟次数。这是因为在开始阶段,许多的变换会将计划朝着目标方向推进,因此在选择方向时不需要特别有信心。随着一个较好的解决方案逐渐接近,计划的走向变得更加重要。将此与从相同起始点开始的分层运行相结合,以确保探索到更广泛的解决方案。

这种方法的优点是我能够控制算法的运行时间。我可以查看模拟运行所需的时间,并相应地调整迭代次数,我还可以选择从起始点开始的次数。每台运行此算法的机器操作会略有不同,一致的算法运行时间对于确保该项目在实际应用中的有效性至关重要。

优化运行的结果。图示由作者提供。

#schedule is {(start_time, end_time): num_agents}

{'Schedule': 
  {(0, 540): 5,
   (30, 570): 2,
   (60, 600): 1,
   (90, 630): 1,
   (120, 660): 4,
   (150, 690): 1,
   (180, 720): 0,
   (210, 750): 1,
   (240, 780): 18,
   (0, 660): 0,
   (30, 690): 0,
   (60, 720): 0,
   (90, 750): 0,
   (120, 780): 3}
'Average Wait': 0.5738814985851588
'Worker Units': 660.0
'Worst Shift Time In Queue, Relative to Target': 0.5965600329117189}

结论

现在我的朋友有一个程序,可以根据他的输入和约束条件为他建立一个时间表。目前尚不清楚这个程序的实际表现如何。我们需要等待收集足够的数据来评估时间表和仿真的准确性。现实世界中会有我们没有考虑到的因素,比如人们生病或度假。但仿真的一个好处是我们可以在以后添加未考虑到的内容。这样不会需要大规模的项目重做。我们只需在需要的地方插入更改即可。

这个项目最重要的部分之一是每个部分都可以独立工作,且非常模块化。我们可以将任何不规则的情况建模并纳入仿真中。许多作业项目因为解决方案的僵化而未能脱离课堂。在我看来,这个项目特别有用,因为它的灵活性和重做能力。现实世界在不断变化,技术解决方案应能够适应以保持相关性。

一个集装箱港口是一个模块化系统。照片由CHUTTERSNAP提供,刊登于Unsplash

最终想法

非常感谢 Jeremy Harper,项目中的朋友。

这个项目激发了我对这个领域的兴趣,我希望能尝试其他不同的问题。如果你有一个需要建模、仿真和优化的现实世界问题,我很愿意了解,并可能在这个领域进一步合作。我的 LinkedIn

参考文献

1. Rahul Awati, Erlang C,techtarget.com

2. Naveen Mahadevan, 服务水平协议 (2022),sprinklr.com

3. N. Jian, S. Henderson, 仿真优化导论 (2015),冬季仿真会议

4. Y. Carlson, A. Maria, 仿真优化:方法与应用 (1997),冬季仿真会议

单一 Python 包以满足 99% 的路径需求

原文:towardsdatascience.com/single-python-package-to-cover-99-of-your-path-needs-babdaf30a1a0

Pathlib: 你一直梦寐以求的库

ArliTowards Data Science Arli

·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 1 月 12 日

--

如果你想亲自体验 Medium,考虑通过 注册会员来支持我和成千上万的其他作者。每月只需 5 美元,这对我们作者支持巨大,并且你可以访问 Medium 上所有精彩的故事。

图片由 Alice Donovan Rouse 提供,来自 Unsplash

操作路径是任何生产项目中的基本任务。你可能需要从远程服务器加载符合特定模式的文件,移动或存储管道中的不同处理过的文件和版本,或者只是读取或写入文件。

在所有这些步骤中,你都会遇到路径的操作。通常,你会发现自己反复在互联网上搜索解决常见问题的方法,例如:

  • 如何从一个文件夹中获取所有文件?

  • 如何检查一个文件夹是否存在?

  • 如何在 Python 中创建一个文件夹?

虽然这些问题的答案很容易找到,但你会遇到整个管道中代码不一致的情况。代码的某些部分可能使用 os.path,另一部分可能使用 shutil,还有一些部分可能使用 glob

为了避免这种不一致性,我建议使用 pathlib 模块,它提供了一种在不同操作系统中处理路径的一致方式,并且提供了面向对象的接口,使其比 os.pathshutil 模块更具可读性、更简单且更一致。

pathlib 和 Path 对象

pathlib 是一个简化 Python 文件路径处理的模块。它提供了一个 Path 类,代表一个文件或目录路径,并提供了一系列方法来对路径执行各种操作。

pathlib的一个关键优点是它对路径操作的面向对象方法。与使用单独的函数来操作字符串路径不同,pathlib提供了一个单一的Path类,具有一系列可以用来对路径执行各种操作的方法。这使得理解和操作路径变得更加容易,因为所有相关功能都包含在一个对象中。

pathlib的另一个优点是它为路径操作提供了统一的接口。它提供了一整套方法,可以用来对不同类型的路径执行各种操作,如本地文件路径、FTP/SFTP 路径等。这使得编写处理不同类型路径的代码变得更容易,因为你不需要为每种路径类型使用不同的函数或库。

为了展示稍后如何操作所有函数,我们需要定义一个指向“origin/data/for_arli”的路径,通过定义一个Path对象来实现:

from pathlib import Path
path = Path('origin/data/for_arli')

你已经准备好使用Path对象的方法了,你会发现它非常简单。

路径存在性和类型

在许多情况下,我们希望检查文件夹或文件在给定路径中是否可用,如果不存在,我们希望进行某些操作,比如引发错误。

 if path.exists():
    print(f"{path} exists.")
    if path.is_file():
        print(f"{path} is a file.")
    elif path.is_dir():
        print(f"{path} is a directory.")
else:
    raise ValueError(f"{path} does not exists")

这段代码将检查路径‘origin/data/for_arli’是否存在,如果存在,它会检查它是文件还是目录。如果路径不存在,它将打印并引发一个错误,指示路径不存在。

文件和目录操作

假设现在我们有兴趣列出路径中的所有文件/文件夹,我们可以这样做:

for f in path.iterdir():
    print(f)

它将遍历路径并打印其中的每个文件或文件夹。你可以将其与之前的is_dir()is_file()方法结合使用,以列出文件或目录。

现在假设我们想要删除路径,我们可以这样做:

path.rmdir()

如你所见,这个命令仅在路径为空时有效。

因此,你需要一种方法在删除文件夹之前移除文件夹中的所有文件:

for f in path.iterdir():
    f.unlink()

在这里,我们使用unlink()删除路径中的文件。现在你可以使用rmdir()毫无问题地删除文件夹。

哦,如果你想重新创建文件夹:

path.mkdir()

注意,mkdir()具有非常有用的参数,如:

  • parents=True 用于创建路径中任何缺失的父级

  • exists=True 如果文件夹已经存在,将忽略任何错误

你也可以使用下面的命令重命名文件或目录:

path.rename('origin/data/for_arli2')

操作和信息

Path的最常用方法之一肯定是joinpath(),用于将路径与字符串连接(它也处理两个Path对象之间的连接):

path = Path("/origin/data/for_arli")
# Join another path to the original path
new_path = path.joinpath("la")
print(new_path)  # prints 'origin/data/for_arli/bla'

有时你可能需要获取文件或文件夹的一些关键统计信息(创建时间、修改时间)或所有者(用户或组)。为此,你可以使用:

print(path.stat()) # print statistics 
print(path.owner()) # print owner

输入/输出

也可以使用pathlib来读取或写入文件。

open()方法用于打开指定路径的文件并返回一个文件对象。此方法的工作方式类似于内置的open():你可以使用文件对象来读取或写入文件。以下是使用write()方法从Path写入文件的示例。

# Open a file for writing
path = Path('origin/data/for_arli/example.txt')
with path.open(mode='w') as f:
    # Write to the file
    f.write('Hello, World!')

请注意,你不需要手动创建example.txt

对于读取操作,原理是相同的,但使用的是read()方法:

path = Path('example.txt')
with path.open(mode='r') as f:
    # Read from the file
    contents = f.read()
    print(contents) # Output: Hello World!

总的来说,pathlib是一个对 Python 开发者非常有用的库,因为它提供了一个面向对象的接口,用于表示文件路径并在不同操作系统上以一致的方式执行各种操作。它使得处理文件路径和目录变得更加方便、直接且易于理解,提供了一组统一的方法。此外,与使用基于字符串的路径操作库如os.pathshutil相比,pathlib的面向对象设计允许编写更具可读性和可维护性的代码。

没有额外费用,你可以通过我的推荐链接订阅 Medium。

[## 通过我的推荐链接加入 Medium — Arli

阅读 Arli 和其他成千上万的 Medium 作家的每一个故事。你的会员费用直接支持 Arli 和……

medium.com](https://medium.com/@arli94/membership?source=post_page-----babdaf30a1a0--------------------------------)

或者你可以将我的所有帖子发送到你的收件箱中。 在这里操作!

如果你想亲自体验 Medium,可以考虑通过 注册会员 来支持我和其他成千上万的作家。这只需每月$5,它极大地支持我们作家,同时你可以访问 Medium 上的所有精彩故事。

奇异值分解(SVD)揭秘

原文:towardsdatascience.com/singular-value-decomposition-svd-demystified-57fc44b802a0

一份包含 Python 示例的 SVD 综合指南

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

·发表于 Towards Data Science ·阅读时间 19 分钟·2023 年 11 月 8 日

--

奇异值分解(SVD)是一种强大的矩阵因式分解技术,将矩阵分解成三个其他矩阵,从而揭示了原始矩阵的重要结构特征。它在信号处理、图像压缩以及机器学习中的降维等广泛应用中都发挥了作用。

这篇文章提供了计算矩阵奇异值分解的逐步指南,包括详细的数值示例。随后,文章演示了如何使用奇异值分解进行降维,并使用 Python 示例说明了这一过程。最后,文章讨论了奇异值分解的各种应用及其一些局限性。

文章假设读者具有基础的线性代数知识。具体来说,读者应该熟悉诸如向量和矩阵范数、矩阵的秩、特征分解(特征向量和特征值)、正交归一向量和线性投影等概念。

图片由 Peggy und Marco Lachmann-Anke 提供,来源于 Pixabay

数学定义

一个 m × n 实数矩阵 A 的奇异值分解是形式为 A = UΣVᵗ 的因式分解,其中:

  • U 是一个 m × m 正交矩阵(即,其列和行是正交归一的向量)。U 的列被称为 A左奇异 向量

  • Σ 是一个 m × n 的矩形对角矩阵,对角线上有非负实数。对角线条目 σᵢ = Σᵢᵢ 被称为 A奇异值,通常按降序排列,即 σ≥ σ≥ … ≥ σₙ ≥ 0。非零奇异值的数量等于 A 的秩。

  • V 是一个 n × n 的正交矩阵。V 的列称为 A右奇异向量

矩阵的 SVD

每个矩阵都有一个奇异值分解(此声明的证明可以在这里找到)。这与特征值分解不同,例如,特征值分解仅适用于平方对角化矩阵。

计算 SVD

矩阵 A 的奇异值分解可以通过以下观察来计算:

  1. A 的左奇异向量是一组 AAᵗ 的正交特征向量。

  2. A 的右奇异向量是一组 AᵗA 的正交特征向量。

  3. A 的非零奇异值是 AᵗAAAᵗ 的非零特征值的平方根。

  4. 如果 UΣVᵗA 的 SVD,那么对于每个奇异值 σᵢ

其中 uU 的第 i 列,vV 的第 i 列。

证明

  1. 我们将首先证明 A 的左奇异向量是一组 AAᵗ 的正交特征向量。

A = UΣVᵗA 的 SVD,接下来检查 AAᵗ 的乘积:

由于 Σ 是一个对角矩阵,对角线上有奇异值 σᵢ,ΣΣ 也是一个对角矩阵,其中每个对角元素是 σᵢ²。我们用 Σ² 来表示这个矩阵。这给我们带来了:

由于 U 是正交的,UᵗU = I,通过将方程的两边右乘 U,我们得到:

现在考虑 U 的一列,记作 u。由于 ABᵢ = [AB](即矩阵 A 乘以矩阵 B 的第 i 列等于它们的积 AB 的第 i 列),我们可以写出:

因此,uAAᵗ 的一个特征向量,对应于特征值

λᵢ = σᵢ²。换句话说,U 的列是 AAᵗ 的特征向量。由于 U 的列是正交的,A 的左奇异向量(U 的列)是 AAᵗ 的一组正交向量。

2. 以类似的方式,我们可以证明 A 的右奇异向量是一组 AᵗA 的正交特征向量。

3. 我们首先注意到AAᵗ是对称且半正定的。因此,它的所有特征值都是实数且非负的,并且有一组完整的正交特征向量。设{u₁, …, u}为对应特征值λ≥ λ≥ … ≥ λₙ ≥ 0 的AAᵗ的正交特征向量。对于任何特征值为λᵢAAᵗ的特征向量u,我们有:

因此,A的奇异值是AAᵗ特征值的平方根。

类似地,我们可以证明A的奇异值也是AᵗA特征值的平方根。

4. 留给读者作为练习。

根据上述观察,我们可以使用以下步骤计算一个m × n矩阵A的 SVD。

  1. 构造矩阵AᵗA

  2. 计算AᵗA的特征值和特征向量。特征值将是A的奇异值的平方,特征向量将形成矩阵V在 SVD 中的列。

  3. 将矩阵A的奇异值按降序排列。创建一个m × n 的对角矩阵Σ,奇异值放在对角线上,如有必要可填充零,以使矩阵与A具有相同的维度。

  4. AᵗA的特征向量归一化为单位长度,并将它们作为矩阵V的列。

  5. 对于每个奇异值σᵢ,计算相应的左奇异向量u为:

其中vV的第i列。将这些向量作为列放入矩阵U中。

如果n < mA是秩亏的(即,rank(A) < min(m, n)),那么没有足够的非零奇异值来确定U的列。在这种情况下,我们需要通过找到额外的正交向量来完成U,这些向量生成Aᵗ的零空间(核)。

Aᵗ零空间,记作N(Aᵗ),是满足Aᵗx = 0的向量x的集合,这些向量也是AAᵗ的特征值为 0 的特征向量(因为Aᵗx = 0⋅x)。要找到N(Aᵗ)的正交基,我们首先解齐次线性系统Aᵗx = 0以找到N(Aᵗ*)的基,然后对这组基向量应用Gram-Schmidt 过程使它们正交,最后将它们归一化为单位向量。

找到A的左奇异向量(U的列)的另一种方法是计算AAᵗ的特征向量,但这种方法通常比使用左奇异向量和右奇异向量之间的关系(观察 4)以及计算Aᵗ的零空间(如有必要)更耗时。

请注意,也可以通过找到左奇异向量(即AAᵗ的特征向量)来开始 SVD 计算,然后使用以下关系找到右奇异向量:

选择使用 AAᵗAᵗA 取决于哪个矩阵较小。

数值示例

例如,让我们计算以下矩阵的 SVD:

UΣVᵗA 的奇异值分解(SVD)。A 的维度是 3 × 2. 因此,U 的大小是 3 × 3,Σ 的大小是 3 × 2,而 V 的大小是 2 × 2。

由于 AᵗA (2 × 2) 的大小小于 AAᵗ (3 × 3) 的大小,因此从 A 的右奇异向量开始是有意义的。

我们首先计算 AᵗA

我们现在找到 AᵗA 的特征值和特征向量。矩阵的特征多项式为:

这个多项式的根是:

AᵗA 的特征值按降序排列为 λ= 9 和 λ₂ = 1. 因此,A 的奇异值为 σ₁ = 3 和 σ₂ = 1,矩阵 Σ 为:

我们现在通过找到 AᵗA 的一组正交标准特征向量来找到右奇异向量(V 的列)。

对应于 λ= 9 的特征向量是:

因此,特征向量的形式为 v₁ = (t, t)。为了得到单位长度的特征向量,我们需要:

因此,对应于 λ₁ =9 的单位特征向量是:

同样,对应于 λ₂ = 1 的特征向量是:

因此,特征向量的形式为 v₂ = (t, t)。为了得到单位长度的特征向量,我们需要

因此,λ₂=1 对应的单位特征向量是:

我们现在可以写出矩阵 V,其列为向量 v₁ 和 v₂:

最后,我们找到 A 的左奇异向量。从观察 4 中可以得出:

由于只剩下一个 U 的列向量,我们可以直接找到一个垂直于 u₁ 和 u₂ 的单位向量,而不是计算 Aᵗ 的核。

u₃ = (a, b, c)。为了垂直于 u₂,我们需要 a = b。然后条件 uu₁ = 0 变为

因此,

为了使向量长度为单位,我们需要

因此,

而矩阵 U 为:

A 的最终完整 SVD 为:

使用 NumPy 计算 SVD

要使用 numpy 计算矩阵的 SVD,可以调用函数 [np.linalg.svd](https://numpy.org/doc/stable/reference/generated/numpy.linalg.svd.html)。给定一个形状为 (mn) 的矩阵 A,该函数返回一个元组 (USVᵗ),其中 U 是一个形状为 (mm) 的矩阵,包含左奇异向量在其列中,S 是一个大小为 k = min(mn) 的向量,包含按降序排列的奇异值,而 Vᵗ 是一个形状为 (nn) 的矩阵,包含右奇异向量在其行中。

例如,使用这个函数来计算之前示例中矩阵的 SVD:

import numpy as np

A = np.array([[1, 0], [0, 1], [2, 2]])
np.linalg.svd(A)

我们得到的输出是:

(array([[-2.35702260e-01,  7.07106781e-01, -6.66666667e-01],
        [-2.35702260e-01, -7.07106781e-01, -6.66666667e-01],
        [-9.42809042e-01, -1.11022302e-16,  3.33333333e-01]]),
 array([3., 1.]),
 array([[-0.70710678, -0.70710678],
        [ 0.70710678, -0.70710678]]))

这是我们通过手动计算得到的相同 SVD 分解,只是符号有所不同(例如,U 的第一列方向已翻转)。这表明,矩阵的 SVD 分解不是完全唯一的。虽然奇异值本身是唯一的,但相关的奇异向量(即 UV 的列)由于以下原因不是严格唯一的:

  1. 如果奇异值重复,则可以选择任何正交归一集来填满相关的特征空间。

  2. 即使奇异值不同,对应的奇异向量也可以乘以 -1(即,它们的方向可以被翻转),仍然可以形成有效的 SVD。

紧凑 SVD

紧凑奇异值分解是完整 SVD 的简化形式,仅保留非零奇异值及其对应的奇异向量。

正式来说,一个 m × n 矩阵 A 的紧凑 SVD(秩为 rr ≤ min{mn}))的分解形式是 A = UᵣΣᵣVᵣᵗ,其中:

  • Uᵣ 是一个 m × r 的矩阵,其列是 A 的前 r 个左奇异向量。

  • Σ 是一个 r × r 的对角矩阵,对角线上是 r 个非零奇异值。

  • Vᵣ 是一个 n × r 的矩阵,其列是 A 的前 r 个右奇异向量。

例如,我们之前示例中的矩阵的秩为 2,因为它有两个非零奇异值。因此,它的紧凑 SVD 分解为:

矩阵 Uᵣ、ΣVᵣ 仅包含重构矩阵 A 所需的基本信息。紧凑 SVD 可以显著节省存储和计算,尤其是对于具有许多零奇异值的矩阵(即,当 r << min{mn})。

截断 SVD

截断(简化)SVD 是 SVD 的一种变体,用于用较低秩的矩阵来近似原始矩阵 A

要创建一个具有秩 r 的矩阵 A 的截断 SVD,我们仅取前 k < r 个最大奇异值及其对应的奇异向量(k 是一个参数)。这给出了原矩阵 Aₖ 的近似,即 Aₖ = UₖΣₖVₖᵗ,其中:

  • Uₖ 是一个 m × k 的矩阵,其列是 A 的前 k 个左奇异向量,对应于 k 个最大的奇异值。

  • Σ 是一个 k × k 的对角矩阵,对角线上是 k 个最大的奇异值。

  • Vₖ 是一个 n × k 的矩阵,其列是 A 的前 k 个右奇异向量,对应于 k 个最大的奇异值。

截断 SVD

例如,我们可以通过仅取最大单一值及其对应向量,将前面例子的矩阵截断到秩 k = 1:

在 NumPy 中,可以使用以下代码片段轻松计算截断 SVD:

U, S, Vt = np.linalg.svd(A)

k = 1  # target rank
U_k = U[:, :k]
S_k = np.diag(S[:k])
Vt_k = Vt[:k, :]

A_k = U_k @ S_k @ Vt_k
A_k
array([[0.5, 0.5],
       [0.5, 0.5],
       [2\. , 2\. ]])

截断 SVD 特别有效,因为截断矩阵 Aₖ 在 Frobenius 范数(最小二乘差异)和 2-范数方面都是矩阵 A 的最佳秩-k 近似,即:

这个结果被称为Eckart-Young-Mirsky 定理或矩阵近似引理,其证明可以在这个维基百科页面中找到。

k 的选择控制了近似精度与表示紧凑性之间的权衡。较小的 k 会导致矩阵更紧凑,但近似更粗糙。在实际数据矩阵中,只有非常小的奇异值较大。在这种情况下,Aₖ 通过保留少量较大的奇异值,可以很好地近似 A

截断 SVD 降维

使用截断 SVD,也可以减少数据矩阵 A 的维度(特征)。为了将 A 的维度从 n 降到 k,我们将矩阵行投影到由前 k 个右奇异向量张成的空间。这是通过将原始数据矩阵 A 乘以矩阵 Vₖ 来完成的:

降维矩阵现在具有 n × k 的维度,并包含原始数据在 k 维子空间上的投影。它的列是降维空间中的新特征。这些特征是原始特征的线性组合,并且彼此正交。

使用我们之前的例子,我们可以将数据矩阵 A 的维度从 2 降到 1,如下所示:

另一种计算降维矩阵的方法基于以下观察:

证明A 的完整 SVD 是 A = UΣVᵗ,因此 AV = UΣ。通过比较方程两边的 j 列,我们得到:

因此,AVₖ 的所有列都等于 UₖΣ 的所有列,因此这两个矩阵必须相等。

使用 UₖΣ 是计算降维矩阵的更高效的方法,因为它只需乘以 m × kk × k 的矩阵,而不是 m × nn × k 的矩阵(k 通常比 n 小得多)。

在我们之前的例子中:

使用截断 SVD 进行降维通常作为机器学习任务(如分类或聚类)之前的数据预处理步骤,这有助于处理维度灾难,减少计算成本,并可能提高机器学习算法的性能。

要对训练完成后的新数据点(例如测试集中的样本)进行降维,我们只需将它们投影到 A 的前 k 个右奇异向量张成的相同子空间上:

请记住,我们的惯例是将每个向量 x 表示为列向量,而数据点以 A 的行形式存储,这就是为什么我们用 Vₖᵗ 左乘 x 而不是右乘 Vₖ

重建误差

评估降维技术效果的一个关键指标叫做重建误差。它提供了一个定量衡量降维过程导致信息丢失的指标。

要测量特定向量的重建误差,我们首先将其投影回由 m 个右奇异向量张成的原始空间。这是通过将 Vₖ 乘以降维后的向量来完成的:

然后我们将重建误差测量为向量重建组件与原始组件之间的均方误差(MSE):

我们还可以通过将降维后的矩阵行投影回由 m 个右奇异向量张成的原始空间来测量整个矩阵 A 的重建误差。这是通过将降维后的 A 乘以 Vₖ 的转置来完成的:

然后,我们可以使用重建矩阵与原始元素之间的均方误差(MSE)或两个矩阵之间差异的弗罗贝纽斯范数:

例如,我们之前示例中降维矩阵的重建矩阵是:

重建误差为:

Scikit-Learn 中的截断 SVD

Scikit-Learn 提供了 sklearn.decomposition.TruncatedSVD 类的高效实现。其重要参数包括:

  • n_components:输出数据的期望维度数量(默认为 2)。

  • algorithm:要使用的 SVD 求解器。可以是以下选项之一:

    1. ‘arppack’ 使用ARPACK 包装器在 SciPy 中计算AAᵗAᵗA的特征分解(以更高效的方式)。ARPACK 是一个迭代算法,能高效地计算大稀疏矩阵的几个特征值和特征向量。

    2. ‘randomized’(默认)使用基于 Halko 等人算法的快速随机 SVD 求解器[1]。

  • n_iter:随机 SVD 求解器的迭代次数(默认为 5)。

例如,让我们演示如何在之前示例中的矩阵上使用此类:

from sklearn.decomposition import TruncatedSVD

svd = TruncatedSVD(n_components=1, random_state=0)
A_reduced = svd.fit_transform(A)
A_reduced

我们得到的输出是减少后的矩阵:

array([[0.70710678],
       [0.70710678],
       [2.82842712]])

示例:图像压缩

奇异值分解可用于图像压缩。尽管图像矩阵通常是满秩的,但其较低的秩通常具有非常小的奇异值。因此,截断 SVD 可以显著减少图像大小,而不会丢失太多信息。

例如,我们将演示如何使用截断 SVD 压缩以下图像:

作者拍摄的照片

我们首先使用函数[matplotlib.pyplot.imread](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.imread.html)将图像加载到 NumPy 数组中:

import matplotlib.pyplot as plt

image = plt.imread('image.jpg')

图像的形状为:

image.shape
(1600, 1200, 3)

图像的高度为 1600 像素,宽度为 1200 像素,具有 3 个颜色通道(RGB)。

由于 SVD 仅适用于二维数据,我们可以分别对每个颜色通道执行它,或者通过将每个颜色通道展平并水平(或垂直)堆叠,将图像从 3D 矩阵重塑为 2D 矩阵。

例如,以下代码片段通过水平堆叠颜色通道将图像重塑为 2D 矩阵:

height, width, channels = image.shape
flat_image = image.reshape(-1, width * channels)

展平图像的形状为:

flat_image.shape
(1600, 3600)

图像矩阵的秩为:

np.linalg.matrix_rank(flat_image)
1600

矩阵是满秩的(因为 min(1600, 3600) = 1600)。

让我们绘制矩阵的前 100 个奇异值:

U, S, Vt = np.linalg.svd(flat_image)

k = 100
plt.plot(np.arange(k), S[:k])
plt.xlabel('Rank of singular value')
plt.ylabel('Magnitude of singular value')

图像中的前 100 个奇异值

我们可以清楚地看到奇异值的快速衰减。这种衰减意味着我们可以有效地截断图像,而不会显著损失精度。

例如,让我们使用 Truncated SVD 将图像截断到秩为 100:

svd = TruncatedSVD(n_components=100)
truncated_image = svd.fit_transform(flat_image)

截断图像的形状为:

truncated_image.shape
(1600, 100)

截断图像的大小仅为原始图像的 100/3600 = 2.78%!

要查看压缩中丢失了多少信息,我们可以测量图像的重建误差。我们将重建误差测量为原始图像与重建图像之间像素值的均方误差(MSE)。

在 Scikit-Learn 中,可以通过调用TruncatedSVD转换器的inverse_transform方法来获得重建图像:

reconstructed_image = svd.inverse_transform(truncated_image)

因此,重建误差为:

reconstruction_error = np.mean(np.square(reconstructed_image - flat_image))
reconstruction_error
29.323291415822336

因此,原始图像和重建图像之间的像素强度的均方根误差(RMSE)只有大约 5.41(相对于像素范围[0, 255]来说很小)。

要显示重建图像,我们首先需要将其重新塑造成原始的 3D 形状,然后将像素值剪裁到[0, 255]范围内的整数:

reconstructed_image = reconstructed_image.reshape(height, width, channels)
reconstructed_image = np.clip(reconstructed_image, 0, 255).astype('uint8')

我们现在可以使用plt.imshow函数显示图像:

plt.imshow(reconstructed_image)
plt.axis('off')

重建图像

我们可以看到,排名为 100 的重建仅丢失了少量细节。

让我们将上述所有步骤放入一个函数中,该函数将给定的 3D 图像压缩到指定数量的维度,然后重建它:

def compress_image(image, n_components=100):   
    # Reshape the 3D image into a 2D array by stacking the color channels horizontally
    height, width, channels = image.shape
    flat_image = image.reshape(-1, width * channels)

    # Truncate the image using SVD
    svd = TruncatedSVD(n_components=n_components)  
    truncated_image = svd.fit_transform(flat_image)

    # Recover the image from the reduced representation
    reconstructed_image = svd.inverse_transform(truncated_image)

    # Reshape the image to the original 3D shape
    reconstructed_image = reconstructed_image.reshape(height, width, channels)

    # Clip the output to integers in the range [0, 255]
    reconstructed_image = np.clip(reconstructed_image, 0, 255).astype('uint8')
    return reconstructed_image

我们现在可以用不同数量的组件调用这个函数,并检查重建结果:

fig, axes = plt.subplots(1, 5, figsize=(10, 50))
plt.setp(axes, xticks=[], yticks=[])  # Remove axes from the subplots

for i, k in enumerate([5, 10, 20, 50, 100]):
    output_image = compress_image(image, k)    
    axes[i].imshow(output_image)
    axes[i].set_title(f'$k$ = {k}')

不同排名的 SVD 重建

正如我们所见,使用过低的排名,如k = 10,可能会导致信息的显著丢失,而排名为 200 的 SVD 几乎与全秩图像不可区分。

除了压缩图像,SVD 还可以用来去除图像中的噪声。这是因为丢弃图像的低阶成分倾向于去除细小的噪声元素,同时保留图像中更重要的部分。

SVD 的应用

SVD 被应用于许多类型的应用程序中,它有助于揭示观察数据的潜在特征。示例包括:

  1. 潜在语义分析(LSA)是自然语言处理中的一种技术,它通过使用 SVD 减少文本数据的维度,从而揭示词语和文本文档之间的潜在关系。

  2. 在推荐系统中,SVD 用于分解用户-项目交互矩阵,揭示有关用户偏好和项目属性的潜在特征,从而帮助预测算法做出更准确的推荐。

  3. SVD 可以用来高效计算摩尔-彭若斯伪逆,这在矩阵不可逆的情况下使用,例如计算线性方程组的最小二乘解而该方程组没有解。

SVD 的局限性

SVD 有几个局限性,包括:

  1. 计算可能会非常密集,特别是对于大矩阵。SVD 的标准(非随机化)实现的运行时间复杂度为O(mn²),如果mn,或者为O(m²n),如果m < n

  2. 需要将整个数据矩阵存储在内存中,这使得它在非常大的数据集或实时应用中不切实际。

  3. 假设数据内部的关系是线性的,这意味着 SVD 可能无法捕捉变量(特征)之间更复杂的非线性交互。

  4. 从 SVD 获得的潜在特征通常不容易解释。

  5. 标准 SVD 无法处理缺失数据,这意味着需要某种形式的填补,这可能会引入数据偏差。

  6. 当新数据到达时,没有简单的方法可以逐步更新 SVD,这在数据频繁变化的动态系统中(如实时推荐系统)是必需的。

最后的说明

除非另有说明,所有图片均由作者提供。

你可以在我的 GitHub 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/svd

感谢阅读!

参考文献

[1] Halko, N., Martinsson, P. G., & Tropp, J. A. (2011). Finding structure with randomness: Probabilistic algorithms for constructing approximate matrix decompositions. SIAM review, 53(2), 217–288。

[2] 奇异值分解,维基百科,自由百科全书, en.wikipedia.org/wiki/Singular_value_decomposition

奇异值分解与特征分解在降维中的比较

原文:towardsdatascience.com/singular-value-decomposition-vs-eigendecomposition-for-dimensionality-reduction-fc0d9ac24a8e

使用两种方法执行主成分分析(PCA)并比较结果

Rukshan PramodithaTowards Data Science Rukshan Pramoditha

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 20 日

--

图片来源:Viktor Peschel 来自 Pixabay

奇异值分解(SVD)和特征分解(ED)都是来自线性代数的矩阵分解方法。

在机器学习(ML)领域,奇异值分解和特征分解都可以作为数据降维的方法。

之前,我们已经详细讨论了 特征分解。今天,我们将更加重点讨论 SVD。

主成分分析(PCA)可以使用这两种方法来执行。PCA 是机器学习中最流行的线性降维技术。SVD 被认为是 PCA 背后的基础数学。流行的机器学习库 Scikit-learn 也在其 PCA() 函数中使用 SVD 来执行 PCA。因此,SVD 在降维中比特征分解更受欢迎。

NumPy 提供了高级且易于使用的函数来执行 SVD 和特征分解。

**Topics included:
----------------**
01\. What is singular value decomposition?
02\. SVD equation and its terms
03\. Singular value decomposition in NumPy - svd() function
04\. What is eigendecomposition?
05\. Eigendecomposition equation and its terms
06\. Eigendecomposition in NumPy - eig() function
07\. Performing PCA using singular value decomposition
08\. Performing PCA using eigendecomposition
09\. Compare the results of both methods
10\. Conclusions

什么是奇异值分解?

奇异值分解(SVD)是一种矩阵分解方法。这是一个来自线性代数的重要数学运算。

有多种方式可以分解(分解 / 拆解)一个矩阵,就像我们可以将数字 16 分解为 2 x 8 = 16、4 x 4 = 16、2 x 2 x 4 = 16、2 x 2 x 2 x 2 = 16 一样。并不是所有的分解方法都同样重要。这取决于具体的使用案例。

同样,我们可以以多种方式对矩阵进行分解,其中一些更为重要。奇异值分解(SVD)和特征分解是矩阵分解中的重要方法。

奇异值分解是将矩阵A分解为三个矩阵乘积的过程,如下方程所示。

SVD 方程 (图片由作者提供)

  • A: 我们对其执行 SVD 的矩阵。

  • U: 一个方阵。这被称为右奇异向量矩阵。

  • Σ: 一个对角矩阵。这被称为奇异值矩阵,其大小与A相同。

  • V^T: 一个方阵。这被称为左奇异向量矩阵。默认情况下,NumPy 的 SVD 函数返回V^T,它是V的转置。

矩阵A可以是方阵也可以是非方阵,因为 SVD 定义于方阵和非方阵。相比之下,特征分解仅对方阵定义。

矩阵Σ包含奇异值,这些值总是非负的。可以包含零值。

非零奇异值的数量等于矩阵A的秩。

NumPy 中的奇异值分解

在 NumPy 中,可以使用svd()函数轻松地执行 SVD。这里是一个示例。

import numpy as np
A = np.array([[2, 4, 1],
              [5, 7, 6],
              [1, 1, 3]])

U, s, Vt = np.linalg.svd(A)

print("A")
print(A)
print("\nU")
print(U)
print("\ns")
print(s)
print("\nVt")
print(Vt)

(图片由作者提供)

NumPy 的svd()函数返回Σ(奇异值矩阵)作为一个向量(由s表示),而不是对角矩阵。该向量包含了矩阵A的所有奇异值。

如果你想直接获得Σ,可以通过以下代码进行一些修改。

S = np.zeros(np.shape(A))
np.fill_diagonal(S,s)
print(S)

(图片由作者提供)

如果你只需要计算奇异值而不需要 U 和 Vt 矩阵,你可以运行以下代码。

s = np.linalg.svd(A, compute_uv=False)
print(s)

(图片由作者提供)

这就是我们如何在 NumPy 中执行 SVD。它比你想象的要简单得多。接下来,我们将进入特征分解部分。

什么是特征分解?

特征分解是另一种重要的矩阵分解方法。

特征分解是将方阵 A 分解为特征值和特征向量的乘积的过程,如下方程所示。

方阵 A 及其特征值和特征向量对之间的关系 (图片由作者提供)

  • A: 我们对其执行特征分解的矩阵。它应该是一个方阵。

  • λ: 一个称为特征值的标量。

  • x: 一个称为特征向量的向量。

矩阵A应该是方阵,因为特征分解仅对方阵定义。

特征值可以是正值或负值。

特征值和特征向量是成对出现的。这种配对被称为 特征对。因此,矩阵 A 可以有多个这样的特征对。上述方程展示了 A 与其一个特征对之间的关系 [ref: 用 NumPy 进行协方差矩阵的特征分解]

NumPy 中的特征分解

在 NumPy 中,可以使用 eig() 函数轻松地执行特征分解。这里是一个示例。

import numpy as np
A = np.array([[2, 4, 1],
              [5, 7, 6],
              [1, 1, 3]])

eigen_vals, eigen_vecs = np.linalg.eig(A)

print("A")
print(A)
print("\nEigenvalues")
print(eigen_vals)
print("\nEigenvectors")
print(eigen_vecs)

(作者提供的图片)

NumPy 的 eig() 函数返回特征值作为一个向量。该向量包含了 A 的所有特征值。

我们对同一个矩阵 A 执行了 SVD 和特征分解。通过查看输出,我们可以说:

奇异值分解和特征分解即使在矩阵为方阵时也不是相同的东西。

使用奇异值分解执行 PCA

PCA 通常通过对标准化数据的协方差矩阵应用 SVD 来执行。标准化数据的协方差矩阵与非标准化数据的相关矩阵完全相同。

在 SVD 之前我们标准化数据,因为奇异值对原始特征的相对范围非常敏感。

为了演示使用 SVD 的 PCA 过程,我们将使用包含 13 个输入特征的 Wine 数据集。

步骤 1: 获取 Wine 数据。

from sklearn.datasets import load_wine

wine = load_wine()
X = wine.data
y = wine.target

print("Wine dataset size:", X.shape)

(作者提供的图片)

步骤 2: 标准化数据。

from sklearn.preprocessing import StandardScaler

X_scaled = StandardScaler().fit_transform(X)

步骤 3: 计算标准化数据的协方差矩阵。

import numpy as np
cov_mat = np.cov(X_scaled.T)

步骤 4: 对协方差矩阵执行 SVD 并获取协方差矩阵的奇异值。

U, s, Vt = np.linalg.svd(cov_mat)
print(s)

奇异值 (作者提供的图片)

这些奇异值的总和等于数据中的总方差。每个奇异值表示每个组件捕获的方差量。为了计算这些内容,我们需要将奇异值转换为解释的方差。

步骤 5: 将奇异值转换为解释的方差。

exp_var = (s / np.sum(s)) * 100
print(exp_var)

(作者提供的图片)

第一个组件捕获了数据中的 36.2% 方差。第二个组件捕获了数据中的 19.2% 方差,以此类推。

步骤 6: 可视化奇异值以选择正确的组件数量

不是所有组件对模型的贡献相同。我们可以丢弃那些对数据方差贡献不大的组件,只保留最重要的组件。为此,我们需要通过创建 累积解释方差图 来可视化所有奇异值。

cum_exp_var = np.cumsum(exp_var)

# a = Number of input features + 1
a = X.shape[1] + 1

import matplotlib.pyplot as plt
plt.bar(range(1, a), exp_var, align='center',
        label='Individual explained variance')

plt.step(range(1, a), cum_exp_var, where='mid',
         label='Cumulative explained variance', color='red')

plt.ylabel('Explained variance percentage')
plt.xlabel('Principal component index')
plt.xticks(ticks=list(range(1, a)))
plt.legend(loc='best')
plt.tight_layout()

plt.savefig("cumulative explained variance plot.png")

累积解释方差图 (作者提供的图片)

因此,很明显前 7 个主成分捕获了数据中约 90%的方差。因此,我们可以选择前 7 个主成分用于葡萄酒数据集。

查看所有选择标准 这里。

使用特征分解进行主成分分析

主成分分析也可以通过对标准化数据的协方差矩阵应用特征分解来进行。

前 3 步与之前相同。因此,我将继续进行第四步。

步骤 1、步骤 2、步骤 3: 与之前相同。

步骤 4: 对协方差矩阵进行特征分解,并获得协方差矩阵的特征值。

eigen_vals, eigen_vecs = np.linalg.eig(cov_mat)
print(eigen_vals)

特征值(作者提供的图片)

特征值与奇异值完全相同。原因在于协方差矩阵是对称的。

标准化葡萄酒数据的协方差矩阵(作者提供的图片)

一般来说,我们可以说:

对于对称矩阵,特征值与奇异值完全相同。

换句话说,

对于对称矩阵,奇异值分解和特征分解是相同的。

svd()函数不同,eig()函数返回的特征值不是按降序排列的。因此,我们需要手动将它们从大到小排序。

# Sort the eigenvalues in descending order
eigen_vals = np.sort(eigen_vals)[::-1]
print(eigen_vals)

按降序排列的特征值(作者提供的图片)

步骤 5、步骤 6: 与之前相同。

我们可以像之前一样可视化特征值。你会得到完全相同的图。

结论

默认情况下,主成分分析是通过使用奇异值分解(SVD)进行的。也可以通过特征分解来进行。由于协方差矩阵是对称的,两种方法给出的结果相同。

一般来说,奇异值分解和特征分解是完全不同的,但对于像主成分分析中使用的协方差矩阵这样的对称矩阵,两者是相同的!

今天的文章到此结束。

如果您有任何问题或反馈,请告诉我。

阅读下一篇(推荐)

有兴趣了解 AI 课程吗?

支持我作为写作者

我希望你喜欢阅读这篇文章。如果你想支持我作为写作者,请考虑 注册会员 以获得对 Medium 的无限访问。每月只需 5 美元,我将获得你的会员费用的一部分。

[## 使用我的推荐链接加入 Medium - Rukshan Pramoditha

阅读 Rukshan Pramoditha 的每个故事(以及 Medium 上其他成千上万的作家的故事)。你的会员费直接…

rukshanpramoditha.medium.com](https://rukshanpramoditha.medium.com/membership?source=post_page-----fc0d9ac24a8e--------------------------------)

加入我的私人邮件列表

不要再错过我讲的精彩故事了。通过 订阅我的邮件列表,你将会在我发布故事后第一时间直接收到。

非常感谢你的持续支持!下篇文章见。祝大家学习愉快!

葡萄酒数据集信息

  • 数据集来源: 你可以在 这里 下载原始数据集。

  • 数据集许可证: 本数据集在 CC BY 4.0 (创意共享署名 4.0) 许可证下提供。

  • 引用: Lichman, M. (2013). UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州欧文:加利福尼亚大学信息与计算机科学学院。

Rukshan Pramoditha

2023–03–20

影响我数据职业生涯的六本书

原文:towardsdatascience.com/six-books-that-have-shaped-my-data-career-8d9db9a40691?source=collection_archive---------0-----------------------#2023-03-29

关于建模、流程和领导力的精彩读物

Chad IsenbergTowards Data Science Chad Isenberg

·

关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 3 月 29 日

--

图片来源于 Emil WidlundUnsplash

在我数据领域的最初阶段,我曾认为自己会成为一名数据科学家,我的首次数据探索集中在学习统计学和线性代数上,而不是软件工程或数据库管理。然而,在职业生涯的早期,我意识到我更喜欢构建数据资产而不是报告或机器学习模型。如果你对那些早期的日子感兴趣,了解我如何发展我的职业生涯,以及对数据新人们的建议,可以看看我早期的文章

在这篇文章中,我想专注于我与书籍和阅读的断断续续的关系。很久以前,我曾追求美国和英国文学的学术生涯。我专攻 19 世纪的美国文学,正如任何在任何领域追求高级学位的人可以告诉你的那样,我读了很多:小说、诗歌、非虚构、关于虚构的非虚构、关于非虚构的非虚构、以及其他变体。我在博士项目开始的约一年后彻底疲惫不堪,与书籍的关系也因此结束了很长时间。

在过去的几年里,我重新开始阅读,而在数据领域的成长和学习欲望是其中的重要部分。我想强调的是,阅读书籍不足以进入这个领域、获得晋升或找到下一份工作;建立职业生涯远不止于积累知识。尽管如此,我认为从你的行业书籍中阅读有两个巨大的职业利益。

  1. 确实可以以一种不同于在工作中学习或观看他人演示技能的方式获得这些想法、概念和知识。你不仅从作者为了创作作品所经历的学科中受益,而且还从你在阅读中的自律中获益。

  2. 阅读是一种社交活动。除了你与作者之间建立的关系,通过阅读一本书,你还与更广泛的社区建立了联系。即使你没有读过下面的书籍,你可能至少听说过其中的一些。正如软件框架驱动对齐一样,领域中的基础文献也能起到类似的作用。

话虽如此,我们来谈谈书籍吧!

《数据仓库工具包》(金博尔&罗斯)

[## 数据仓库工具包,第 3 版 - 金博尔集团]

Wiley, 2013 工具和实用程序 注:你可能需要“另存链接为”来下载文件。第三章样本数据…

www.kimballgroup.com

我不会掩饰重点。如果你从事数据工作,至少需要对维度建模概念有所了解,而我个人认为没有比直接从源头获取更好的方法了。仅仅把这本书放在你的书架上,就有可能使你成为更优秀的数据专业人士;它实在是太重要了。

当我作为数据分析师第一次拿起这本书时,对维度建模完全是陌生的,它带来了巨大的转变。我最终从头到尾读完了这本书(虽然我不推荐这样做;详见下文),这时候我知道自己想要从报告和分析转向建模和构建。

虽然这本书在实施细节上较为轻描淡写,但概念却是非常扎实的。简单的4 步设计过程 即使在几十年后依然出奇有效。一旦你理解了维度、事实及其如何协同工作来描述业务流程的基本原理,你将拥有一套很棒的工具来开始解决业务问题。

我建议你在深入阅读之前查看一下Holistic.io 的读者指南。我认为整本书都值得阅读,但你阅读那些后面的章节的时间将取决于你遇到的具体数据建模问题。而且,虽然它现在不再维护,Kimball Group Articles & Design Tips仍然充满了出色的想法和方法。

《设计数据密集型应用》(Kleppmann)

## 设计数据密集型应用

通过 O'Reilly 的免费 10 天试用,你可以获得《设计数据密集型应用》及其他 60K+本书籍的完整访问权限。

www.oreilly.com

几年前,我有幸参加了由1904labs赞助的为期 8 周的数据工程研讨会,这是在圣路易斯地区的一家咨询公司。那时我的职业生涯中,只接触过 SQL 和 R,因此这感觉像是向前迈出了巨大的一步。在课程中,我们获得了 Kafka、Scala、Spark、HBase 和 Hive 的实践经验,我深受吸引。我想了解更多关于数据资产构建的基础平台和技术。

DDIA 可能是数据领域第二最知名的文本,这也是有充分理由的。Kleppmann 在可访问性和深度之间达到了惊人的平衡。作为一个计算机科学接触较少的人,我觉得跟随他的编码、数据库设计(及权衡)和分布式架构解释非常容易。同时,我对各种数据技术的工作方式有了真实的了解。

你不一定要从头到尾阅读这本书,但这本书有一种叙事结构,使这种阅读策略非常有效。比起技术细节,我发现数据技术的历史是最重要的收获;你可以理解工具背后的动机。我们为什么有关系数据库管理系统?文档存储解决了什么问题?为什么使用 Spark 而不是 Hadoop?实际操作将为你提供最深入的见解,但DDIA 是紧随其后的选择。

DevOps 手册(Kim, Humble 等)

[## DevOps 手册

这本获奖且畅销的数字化转型商业手册现在已全面更新和扩展…

itrevolution.com](https://itrevolution.com/product/the-devops-handbook-second-edition/?source=post_page-----8d9db9a40691--------------------------------)

在我职业上遇到困难时,我发现这本书极具激励性。我当时在一个传统的 BI 部门,尽管我们做得很好,但我渴望深入流程。我与该运动在精益丰田生产方式中的根源有着强烈的认同感:这些改进方法是在我医疗行业的工作中接触到的。

“三种方式”:流动、反馈以及持续学习和实验,广泛适用于软件开发领域,而数据也变得越来越相关。无论你是开发者还是经理,你都会对痛点和解决方案点头称赞。关于信任和安全的部分似乎尤其适用于现代数据环境,我们面临的一些最大挑战在于:a)提供高质量的数据和 b)让利益相关者接受数据确实是高质量的!

这本书充满了优秀的案例研究,展示了进行 DevOps 转型中的回报和挑战。正如你可能预期的那样,流程很重要,但文化变革才是最终目标。让你的价值链获得广泛的业务对齐既困难又强大;最终,这就是让每个人共同参与设计、开发、交付和操作软件解决方案,以最有效(和安全)的方式进行。

数据库可靠性工程(Campbell & Majors)

## 数据库可靠性工程

IT 领域的基础设施即代码革命也在影响数据库管理。通过这本实用的书……

www.oreilly.com

我在这里开始了对流程工作的痴迷。在我的 BI 开发者工作中,我们使用Redgate工具来管理我们开发的某些方面。在某个时候,我对我们没有使用的一些工具产生了浓厚的兴趣,尤其是Flyway,应用版本控制到我们的数据库对象似乎解决了一些直接的痛点。

这使我打开了这本简明但优秀的书,介绍了如何将站点可靠性工程实践应用于数据库。对我来说,最有价值的收获是将可靠性思维模式应用于数据存储。如何用“黄金镜像”来标准化数据库?错误预算是什么样的?正确的数据库仪表、监控和可观测性是什么样的?简而言之,在你的数据库/数据仓库中,真正重要的是什么?

作者们做得最好的就是提供了一个愿景,将 DBA 角色从单纯的运维角色提升到运维和赋能的结合体。是的,你仍然需要能够提供数据存储运维支持的人,但在使其民主化的过程中可以获得巨大的价值。当各个开发团队可以安全且轻松地创建“批准的”资源时,你可以减少他们的摩擦,同时释放你的数据库团队,以进一步完善自动化和流程,这些都增加了安全性。

Charity Majors 贴纸真是太棒了:

团队拓扑(Skelton & Pais)

## 书籍 - 团队拓扑

团队拓扑 - 作者:Matthew Skelton 和 Manuel Pais 有效的软件团队对任何组织来说都是必不可少的……

teamtopologies.com

随着我职业生涯的发展,我对流程及其如何应用于技术解决方案越来越感兴趣。别误会;技术对我来说仍然非常重要,但我也想看到它的实际应用。探索一个代码库和玩一个演示是一回事,而看到一种技术如何帮助一个组织实现其目标则是另一回事。

为了这个目的,我终于在无数推荐和赞誉后拿起了这本书,我只能说它确实名副其实。团队拓扑学如果说有什么的话,就是一个关于康威定律的案例研究,作者不懈地推动系统化。我认为他们对团队类型和互动的观察尤其有见地,为更好的项目和更好的软件提供了路线图。

数据产品特别容易受到孤岛效应和碎片化的影响。数据源拥有者、工程师、分析师和业务利益相关者通常在不同的区域内工作,彼此联系不足,无法应对开发过程中的发现或不断变化的需求。合理安排团队和设计合适的接口可以大大减少在设计、开发和最终发布数据资产过程中的摩擦。

员工工程师:超越管理轨道的领导力(Larson)

## 员工工程师:超越管理轨道的领导力

为什么写这本书?关于高级技术领导者的资料并不多,尤其是管理职业之外的……

staffeng.com

在我之前的医疗保健职业生涯中,我最终发现自己从事人员管理,这让我感到压力大且不满足。我的不满促使我在五年前涉足数据领域。在这段时间里,我学到了比我预期的更多的东西,我非常高兴自己做出了这个转变。在我职业生涯的这一阶段,我已经减缓了不断学习的步伐;我能够识别模式、主题和工作方式。最近,我开始更多地关注自己未来几十年的职业发展。我希望我的工作既有趣,又有影响力。

Will Larson 的员工工程网站和相关书籍是任何对软件有严肃兴趣的人必读的书,无论是哪个级别(从入门到员工+)还是路径(个人贡献者与管理者)。这本书确实提供了一些结构,如定义员工原型写作的重要性以及如何向关键利益相关者表达你的技术愿景;然而,书中还包含了来自不同背景的员工工程师的丰富案例研究,我发现这些是最有趣的部分。

重申一下,无论你是否想成为一名员工工程师,我认为这本书都很重要,因为它让你思考技术领导力和影响力。刚开始你的职业生涯时,交付和执行似乎是唯一重要的事情。虽然这些是重要的成功标准,但它们远不足以确保你和你的团队 a) 从事对推动业务有意义的工作,以及 b) 因这种影响而得到认可和奖励。

我很高兴重新点燃了与书籍的关系,不仅是为了帮助我的职业生涯的非虚构书籍,还有小说。我现在虽然不如年轻时那样贪婪地阅读,但在过去几年中我读了不少优秀的文学作品:《引力虹》《苍白的火焰》《雪崩》《炼金术士》《荒原》等等。我知道阅读不是每个人的杯茶,我不会坚持说它对你的生活和职业生涯至关重要,但就我个人而言,我不知没有一些好书该怎么办。

Sketch: 一个有前景的 AI 库,帮助直接在 Jupyter 中处理 Pandas 数据框

原文:towardsdatascience.com/sketch-a-promising-ai-library-to-help-with-pandas-dataframes-directly-in-jupyter-5162c32c66ef

在 Jupyter Notebook 中利用 AI 的力量

Andy McDonaldTowards Data Science Andy McDonald

·发布于Towards Data Science ·阅读时间 7 分钟·2023 年 2 月 21 日

--

来自 Pexels 的照片,作者:Tara Winstead

最近几个月,利用 AI 和大型语言模型创建类似于 ChatGPT 的互动聊天机器人受到广泛关注。我们能够直接从 Python 库中在 Jupyter Notebook 中使用这些模型的能力只是时间问题。

一款最近推出的 Python 库 Sketch 将 AI 编码助手直接带到 Python,并可以在 Jupyter 笔记本和 IDE 中轻松使用。该库旨在让用户更容易理解和探索存储在pandas数据框中的数据,而无需额外的插件。

Sketch 库可以快速总结存储在数据框中的数据。它通过使用近似算法(称为数据草图)创建数据摘要,然后将生成的摘要传递给大型语言模型来实现。

通过自然语言输入和可用的功能,我们可以探索我们的数据集。这在很多方面都很有帮助,例如:

  • 为非编码用户创建一个应用程序以探索数据

  • 快速获取创建图表和管理数据的代码

你可以在 PyPi 上找到更多关于这个库的信息:

[## sketch

Sketch 是一个 AI 代码编写助手,专为 pandas 用户设计,理解你数据的上下文,大大提高了…

pypi.org](https://pypi.org/project/sketch/?source=post_page-----5162c32c66ef--------------------------------)

在本文中,我们将探索 sketch 中的两个可用函数:.ask.howto函数。这些函数允许我们用自然语言提问有关数据框的问题以及如何操作它们,而不是使用内建的 pandas 函数。

在撰写本文时,Sketch 库仅有几个月大,版本为 0.3.5,仍在积极开发中。

导入库和数据

第一步是将 sketch 和pandas导入到我们的笔记本中。

import sketch
import pandas as pd

接下来,我们将使用 pandas 的read_csv函数从 CSV 文件中加载数据。在此函数中,我们将传递文件位置和名称。

df = pd.read_csv('Data/Xeek_Well_15-9-15.csv')

当数据加载完成后,我们可以通过调用df来检查其内容。

包含井测量数据的 Pandas 数据框。图片由作者提供。

现在我们已经加载了数据,可以开始在笔记本中使用 sketch 了。

在导入 sketch 之后,将有三个新的函数附加到数据框对象上。这些函数是df.ask()df.howto()df.apply()。在本文中,我们将重点关注askhowto方法。

用.sketch.ask 提问

.ask()方法允许我们使用简单的语言提问有关数据框的问题。我们可以利用这个方法来帮助我们理解和探索数据。

为了尝试一下,我们将询问数据框中的 GROUP 列中有多少个唯一值。该列应包含 7 个不同的地质组。

df.sketch.ask('How many unique values are in the GROUP column?')

Sketch 将返回以下内容:

7

我们可以扩展查询,并要求 Sketch 除了显示 GROUP 列中的唯一值外,还显示这些值的数量:

df.sketch.ask("""How many unique values are in the 
GROUP column and what are the values?""")

Sketch 返回了以下内容。你会看到这更像是句子形式,而不仅仅是一个数字和列表。这对于我们想直接将输出复制到报告中非常方便。

The GROUP column has 7 unique values: 'NORDLAND GP.', 'VESTLAND GP.', 
'TROMS GP.', 'FINNMARK GP.', 'SVALBARD GP.', 'BARENTS SEA GP.', and 
'NORTH SEA GP.'.

尽管如此,当对 FORMATION 列提出相同问题时,AI 助手变得困惑:

df.sketch.ask("""How many unique values are in the 
FORMATION column and what are the values?""")

这返回了一个岩石类型的列表,而不是包含地质形成的列表。

The FORMATION column has 15093 unique values. 
The values are: ['nan', 'SHALE', 'SANDSTONE', 'LIMESTONE', 
'DOLOMITE', 'CHALK', 'ANHYDRITE', 'GYPSUM'].

我们可以通过调用df.FORMATION.unique()来检查 FORMATION 列中实际存在的内容。当我们这样做时,我们会得到一个形成名称的数组,这是符合预期的。

array([nan, 'Utsira Fm.', 'Frigg Fm.', 'Balder Fm.', 'Sele Fm.',
       'Lista Fm.', 'Tor Fm.', 'Hod Fm.', 'Blodoeks Fm.', 'Draupne Fm.',
       'Heather Fm.', 'Skagerrak Fm.'], dtype=object)

我们还可以要求 Sketch 提供一些数据统计信息。在此示例中,我们将获取 GR(伽马射线)列的最小值、最大值和均值。

df.sketch.ask('What is the min, max and mean values for the GR column')

Sketch 返回了以下内容:

The min value for the GR column is 6.0244188309, 
the max value is 804.2989502 and the mean value is 57.9078450037.

乍一看,这似乎是合理的。然而,当我们调用df.describe()并查看 pandas 的汇总统计信息时,我们可以看到均值 59.1542 与 Sketch 返回的 57.9078 不同。

使用.describe 方法创建的 Pandas 数据框总结。图片由作者提供。

这可能是代码中的一个错误导致的结果吗?可能是。

询问 Sketch 如何使用 sketch.howto 做事

Sketch 允许我们询问如何使用我们的数据框,并将返回一个代码块。如果你想快速绘制数据而不必记住所有 matplotlib 调用,这非常方便。

在这个例子中,我们将询问它如何创建一个简单的密度与中子孔隙度散点图,这是岩石物理学中常用的图表。

df.sketch.howto("""How to make a scatterplot with NPHI on the x axis,
caled from 0 to 0.8, and RHOB on the y axis reversed scaled from 1 to 3""")

这返回了一些简单的 matplotlib 代码,可以复制粘贴到下一个单元格中:

import matplotlib.pyplot as plt

# Create the scatterplot
plt.scatter(df['NPHI'], df['RHOB'])

# Set the x-axis limits
plt.xlim(0, 0.8)

# Reverse the y-axis limits
plt.ylim(3, 1)

# Show the plot
plt.show()

执行后,生成以下图表:

从 Sketch 返回的代码生成的基本 matplotlib 图。图片由作者提供。

我们可以看到,上述图表非常简单,因此让我们通过使用第三个变量来为图表添加一些颜色。

df.sketch.howto("""How to make a scatterplot with NPHI on the x axis, 
scaled from 0 to 0.8 and RHOB on the y axis reversed scaled from 1 to 3\. 
Colour the points by GR and add a colorbar.
Limit the colorbar values to between 0 and 200.""")

返回以下代码

 import matplotlib.pyplot as plt

# Create the scatterplot
_['NPHI'].plot.scatter(x='NPHI', y='RHOB', c='GR', cmap='viridis', 
                        vmin=0, vmax=200, s=20, figsize=(10,8))

# Reverse the y axis
plt.gca().invert_yaxis()

# Scale the x axis
plt.xlim(0, 0.8)

# Add a colorbar
plt.colorbar()

它返回的代码几乎是正确的。然而,它对代码添加了一些奇怪的内容。

它没有扩展上述代码,而是转而使用了 pandas .plot 方法,并将其应用于 _['NPHI']。它还忽略了我的 y 轴刻度范围,但正确地进行了反转。

最后,它还添加了 plt.colorbar 的调用,这其实并不必要,并且如果包含会抛出错误。

通过对代码进行一些修正,我们可以使其正常工作:

import matplotlib.pyplot as plt

# Create the scatterplot
df.plot.scatter(x='NPHI', y='RHOB', c='GR', cmap='viridis', 
                        vmin=0, vmax=200, s=20, figsize=(10,8))

# Reverse the y axis
plt.gca().invert_yaxis()

# Scale the x axis
plt.xlim(0, 0.8)

从 Sketch 生成的代码创建的图,经过修改。图片由作者提供。

现在我们有一个很好的图表可以用于进一步构建。

总结

sketch 库在将 AI 的强大功能集成到 Jupyter Notebook 或 IDE 中看起来非常有前途。尽管在撰写本文时出现了一些问题,但我们必须记住这仍然是一个较新的库,仍在积极开发中。接下来几个月这个库的发展将非常值得关注。

与当前任何基于 AI 的工具一样,始终需要谨慎,特别是在依赖它生成的答案时。然而,即使有这种谨慎,这些系统也可以带来诸多好处,包括在你忘记函数调用时帮助回忆,或者无需编写代码即可快速创建图表。

有趣的是,如果将其与像 Streamlit 这样的仪表板或应用程序集成,这可能为非编码人员提供一个强大的工具。

本文使用的数据集是 Xeek 和 FORCE 2020 机器学习比赛的训练数据集的一个子集 (Bormann et al., 2020)。该数据集以挪威政府的 NOLD 2.0 许可协议发布,详情见这里:挪威开放政府数据许可 (NLOD) 2.0。完整数据集可以通过 这里 访问。

数据集的完整参考是:

Bormann, Peter, Aursand, Peder, Dilib, Fahad, Manral, Surrender, & Dischington, Peter. (2020). FORCE 2020 深度测井和岩性数据集用于机器学习竞赛 [数据集]。Zenodo。 doi.org/10.5281/zenodo.4351156

感谢阅读。在你离开之前,你应该订阅我的内容,并将我的文章发送到你的收件箱。 你可以在这里做到这一点!或者,你可以 注册我的通讯 以免费获取额外的内容直接发送到你的收件箱。

其次,你可以通过注册会员获得完整的 Medium 体验,并支持我和其他成千上万的作者。它每月仅需 $5,你可以完全访问所有精彩的 Medium 文章,还能通过写作赚钱。如果你使用 我的链接, 你将直接通过你的费用的一部分支持我,而且不会额外花费你更多。如果你这样做,非常感谢你的支持!

带有异常值的偏度和峰度

原文:towardsdatascience.com/skewness-and-kurtosis-with-outliers-f43167532c69

R 统计系列

Md Sohel MahmoodTowards Data Science Md Sohel Mahmood

·发布于 Towards Data Science ·6 分钟阅读·2023 年 5 月 10 日

--

图片来源:Aaron BurdenUnsplash

介绍

现实世界的数据通常包含极端值,这可能导致偏斜分布。偏斜的数据不适合许多统计分析。单个异常值可能会大幅改变分布的整体统计特性。因此,这些极端值需要谨慎处理。如果这些异常值没有正当理由,一般的指导方针是将其去除。本文将探讨异常值对偏度以及峰度的影响。

下图展示了一个正态分布数据的示例,这种数据适用于多种统计分析。

图片来源:作者

偏度

在统计学中,偏度是衡量非对称分布的指标。基本上,它描述了钟形曲线从对称形式的扭曲程度。偏度可以分为两种类型:

  1. 显示正偏度的分布,其右侧尾部比左侧尾部更长或更分散。分布的均值大于其中位数。

  2. 负偏度的指示是分布的尾部在左侧比右侧更长或更分散。因此,均值小于中位数。

在统计分析中,识别和分析偏斜分布很重要,因为它们可能有显著的影响。

图片来源:作者

图片来源:作者

峰度

峰度描述了分布尾部相对于峰值的形状。除了衡量分布的平坦度外,它还衡量了数据在均值周围的集中程度。峰度可以分为三种类型:

  1. 中峰度型分布的峰度等于零,意味着它们是具有钟形曲线的正态分布。

  2. 它的特点是比正态分布具有更高的峰值和更重的尾部,因为它的峰度大于零。与正态分布相比,这表明数据在均值周围更加集中。

  3. 在低峰度分布中,峰值比正态分布更平坦,尾部也更轻,因为峰度小于零。因此,相比于正态分布,数据在均值周围的集中度较低。

作者提供的图片

作者提供的图片

异常值

现在处理实际世界的数据,这些数据大多是偏斜的,通常包含异常值。我们之前讨论过异常值检测程序。一种常见的策略是 IQR(四分位距)方法,这是行业标准。下面的文章是使用 python 编写的。

Python 中异常值检测的实际应用

IQR、Hampel 和 DBSCAN 方法

towardsdatascience.com

确定上下限的 IQR 方法如下。

  • 找到第一个四分位数,Q1。

  • 找到第三个四分位数,Q3。

  • 计算 IQR。IQR= Q3-Q1。

  • 定义正常数据范围,其中下限为 Q1–1.5IQR,上限为 Q3+1.5IQR。

  • 任何超出此范围的数据点都被视为异常值,应在进一步分析中删除。

数据集

本案例研究的数据来源将是 UCI 机器学习库的成人数据集。根据数据集中的种族、教育程度、职业、性别、薪资、每周工作小时数以及收入,应该识别出约 30000 人。

让我们检查年龄的分布。

作者提供的图片

这似乎在右侧有些偏斜,并且右侧还有一些可能的异常值。让我们在 R 中确定偏度和峰度。

在 R 中的实现

偏度和峰度公式

偏度和峰度测量的公式如上所示。这里,µ = 样本均值,σ = 样本标准差。在 R 中,我们可以定义一个函数或使用 moments 库来计算偏度,如代码所示。使用 moments 库确定的偏度值为 0.2213737,而使用公式计算的偏度为 0.2211937,这两个值非常接近。由于值大于 0,分布是正偏的。

现在是重点。我们想要去除异常值并查看分布是否仍然偏斜。让我们修剪顶部 2.5% 和底部 2.5% 的数据以排除极端数据点。在这种情况下,分位数偏度定义为:

从年龄数据中计算出的分位数偏度为 0.09677419,虽然仍为正值,但幅度较小。如果我们排除顶部 1% 和底部 1%,偏度将为 0.1304348,这也是正值,但相比于原始数据幅度较小。105 的修剪将得到 0.0212766 的偏度值。修剪 25% 会使其变为负偏,但在实际操作中,修剪 25% 的数据并不实用。因此,显然修剪的数据越多,数据的偏度越小。

1% 修剪 → 偏度 0.1304348

2.5% 修剪 → 偏度 0.09677419

10% 修剪 → 偏度 0.0212766

接下来,我们可以对峰度进行相同的研究。使用 moments 库和公式计算的原始峰度值分别为 2.298557 和 2.296066。正态分布的峰度为 3。如果峰度大于 3,分布被认为是尖峰的;如果峰度小于 3,分布被认为是平峰的。

我们可以使用下面的公式进行基于分位数的峰度分析。

在这里,Q₁ 和 Q₃ 是分布的第一个和第三个四分位数值。Qₚ 是 p 处的分位数。当 p=0.025(基本上从顶部修剪 2.5% 和从底部修剪 2.5%)时,分位数峰度是 95% 内四分位区间与四分位范围的比率。年龄的分位数峰度计算值为 2.214286,也小于 3。因此,去除异常值对峰度值的影响不大。

与其决定性地修剪固定比例的数据,不如使用 IQR 方法来去除异常值。

这个过程给出了偏度和峰度分别为 0.1967011 和 2.203808。

结论

我们已经涵盖了偏度和峰度的基本概念并在 R 中实现了它。处理有异常值的实际数据需要进行一些清理以去除那些极端值。讨论了基于分位数的偏度和峰度测量,并在 R 中实现了行业标准的异常值检测方法(IQR)。读者需要根据自己的判断选择异常值去除方法。

数据集致谢

Dua, D. 和 Graff, C. (2019). UCI 机器学习库 [http://archive.ics.uci.edu/ml]. 加州尔湾: 加州大学信息与计算机科学学院。

感谢阅读。

请我喝杯咖啡

[## 使用我的推荐链接加入 Medium - Md Sohel Mahmood

阅读 Md Sohel Mahmood 的每一篇故事(以及 Medium 上其他成千上万位作者的故事)。你的会员费将直接…

获取每当 Md Sohel Mahmood 发布新内容时的邮件通知 [## 获取每当 Md Sohel Mahmood 发布新内容时的邮件通知。

每当 Md Sohel Mahmood 发布新内容时,你会收到邮件通知。如果你还没有 Medium 账户,注册时将创建一个账户…

使用我的推荐链接加入 Medium - Md Sohel Mahmood

现代 ML 工程师的 Sklearn 管道:你不能忽视的 9 种技术

原文:towardsdatascience.com/sklearn-pipelines-for-the-modern-ml-engineer-9-techniques-you-cant-ignore-637788f05df5

你可以用很多方法来构建它们……

Bex T.Towards Data Science Bex T.

·发表于 Towards Data Science ·10 分钟阅读·2023 年 5 月 29 日

--

由我使用 Midjourney 制作的图像

动机

今天,这就是我所销售的:

awesome_pipeline.fit(X, y)

awesome_pipeline 可能看起来只是另一个变量,但它在幕后对 Xy 的作用如下:

  1. 自动隔离 X 的数值特征和分类特征。

  2. 填充数值特征中的缺失值。

  3. 对偏斜特征进行对数变换,同时对其余部分进行归一化。

  4. 填充分类特征中的缺失值,并对其进行一热编码。

  5. 对目标数组 y 进行归一化,以确保准确。

除了将几乎 100 行不可读的代码压缩成一行之外,awesome_pipeline 现在还可以插入到交叉验证器或超参数调整器中,保护你的代码免受数据泄漏,并使一切变得可重复、模块化且无头痛。

让我们看看如何构建它。

0. 估计器 vs 变换器

首先,让我们了解一下术语。

在 Sklearn 中,变换器是任何接受数据集特征、应用变换并返回结果的类或函数。它具有 fit_transformtransform 方法。

一个例子是 QuantileTransformer,它接受数值输入并将其转换为正态分布。对于具有 异常值 的特征特别有用。

变换器继承自 TransformerMixin 基类。

from sklearn.base import TransformerMixin
from sklearn.preprocessing import QuantileTransformer

isinstance(QuantileTransformer(), TransformerMixin)
True

另一方面,估计器是任何通常会对数据集生成预测的类。估计器的名称通常以 RegressorClassifier 结尾。

估计器继承自 BaseEstimator 类。

Estimators inherit from the BaseEstimator class
True

1. 原始管道

Sklearn 中的基本管道始终由一个或多个相同类型的变换器和一个最终的估计器组成。它将变换器串联起来,对特征数组(X)执行一系列操作,避免了对每个变换器调用 fit_transform 的需求,并将最终输出传递给估计器。所有这些操作都在一行代码中完成。

from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler

from sklearn.pipeline import make_pipeline

# Define the numeric pipeline
numeric_pipeline = make_pipeline(
    StandardScaler(), SimpleImputer(), LinearRegression()
)

numeric_pipeline.fit(only_numeric_X, y)

要构建一个基本管道,你可以使用 make_pipeline 函数并传入变换器和估计器。变换器的顺序很重要。

上述示例展示了一个数字管道,它只能应用于包含数字特征的数据集。还有一个分类管道,专为仅包含分类特征的数据集设计:

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import OneHotEncoder

# Define the categorical pipeline
categorical_pipeline = make_pipeline(
    SimpleImputer(strategy="most_frequent"),
    StandardScaler(),
    LogisticRegression(),
)

传入 make_pipeline 的每一项都被称为管道中的一个步骤,如下输出所示:

numeric_pipeline
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('simpleimputer', SimpleImputer()),
                ('linearregression', LinearRegression())])

make_pipeline 函数会自动为管道中的每一步分配名称,但这些名称可能会很长且显式。

如果你想提供自定义步骤名称,你需要直接使用 Pipeline 类:

from sklearn.pipeline import Pipeline

numeric_pipeline = Pipeline(
    steps=[
        ("scale", StandardScaler()),
        ("impute", SimpleImputer()),
        ("lr", LinearRegression()),
    ]
)

steps 参数接受一个包含两个项目的元组列表:

  1. 步骤名称,作为字符串。

  2. 该步骤的变换器或估计器。

正确命名步骤的重要性将在接下来的部分中显现出来。

2. 变换器的奶昔

在实际应用中,你很少会单独使用基本变换器,因为现实世界的数据集通常包含数字特征和分类特征的混合。

因此,你需要一种方法将不同类别的变换器组合成一个对象,同时指定哪些变换器应应用于数据集 X 的哪些列。

这一功能在 ColumnTransformer 类中得到了优雅的实现。

在第 0 步中,你需要分别定义数字特征和分类特征:

nums = ["numeric_1", "numeric_2", "numeric_3"]
cats = ["categorical_1", "categorical_2", "categorical_3"]

在第 1 步中,为数字特征和分类特征分别定义两个仅包含变换器的管道:

numeric_pipe = make_pipeline(SimpleImputer(), QuantileTransformer())
categorical_pipe = make_pipeline(
    SimpleImputer(strategy="most_frequent"), OrdinalEncoder()
)

然后,你可以创建一个 ColumnTransformer 类的实例:

from sklearn.compose import ColumnTransformer

transformers = ColumnTransformer(
    transformers=[
        ("numeric", numeric_pipeline, nums),
        ("categorical", categorical_pipeline, cats),
    ]
)

ColumnTransformertransformers 参数接受一个包含三项元组的列表:

  1. 步骤的名称。

  2. 变换器或变换器的管道。

  3. 应应用变换器的列名称。

当你使用 transformers 对象时,它会对数字特征和分类特征分别应用两种类型的操作,然后将结果合并以返回一个单一的矩阵。

因此,ColumnTransformer 代表了一个更复杂的管道,它不包括最终的估计器。要完成管道,我们需要添加一个。

3. 顶部有西瓜的奶昔

目前,我们的半成品管道仅转换数据集 X

X_transformed = transformers.fit_transform(X)

唯一缺少的是一个估计器。这很容易解决:

full_pipeline_reg = make_pipeline(transformers, LinearRegression())

# You can also use `Pipeline` class for named steps
full_pipeline_clf = Pipeline(
    steps=[
        ("preprocess", transformers),
        ("clf", LogisticRegression()),
    ]
)

根据机器学习任务的不同,你需要将回归器或分类器估计器串联为管道中的最终步骤。生成的管道将具有 fitpredict 方法,具体取决于任务。

# y is a classification label
full_pipeline_clf.fit(X, y)

# y is a numeric label
full_pipeline_reg.fit(X, y)

4. 选择风格化的列

在定义 ColumnTransformer 时,我们逐一手动指定了数值和分类特征。就像一个原始人一样。

但不用担心!Sklearn 提供了一种更高效的方法来实现这一点。

import numpy as np
from sklearn.compose import make_column_selector

numeric_cols = make_column_selector(dtype_include=np.number)
categoricals = make_column_selector(dtype_exclude=np.number)

make_column_selector 是一个方便的函数,它允许你以各种方式从数据框中自动隔离列。在上面的示例中,我们使用它根据数据类型过滤列。然而,你也可以利用 pattern 参数来指定用于过滤列名的正则表达式(RegEx)模式。

这是一个示例:

pattern = "^(word1|word2)"
filtered_columns = make_column_selector(pattern)

提供的示例捕获了以 word1word2 开头的列。

这个功能在构建 ColumnTransformer 对象时特别有用。它消除了手动列出每一个列名的需求,这在处理包含大量列的数据集时可能变得具有挑战性,甚至是不可能的。

from sklearn.compose import make_column_transformer

# Automatically capture cols based on dtype
nums = make_column_selector(dtype_include=np.number)
cats = make_column_selector(dtype_exclude=np.number)

# Build the pipelines
numeric_pipe = make_pipeline(...)
categorical_pipe = make_pipeline(...)

transformers = make_column_transformer(
    (nums, numeric_pipe), (cats, categorical_pipe)
)

make_column_transformer 函数是一个简写函数,类似于 make_pipeline,它允许你构建 ColumnTransformer 对象而无需明确指定步骤名称。通过将其与 make_column_selector 结合使用,你可以显著缩短代码。

5. 视觉管道

当你打印一个复杂的管道,例如 full_pipeline_clf,输出可能在你的 Jupyter notebook 中变成难以阅读的混乱。

为了解决这个问题,你可以使用 set_config 函数将 display 选项设置为 diagram

from sklearn import set_config

set_config(display="diagram")

现在,当你打印或返回管道时,将显示一个 HTML 图示,提供管道的视觉表示:

这种视觉表示对于调试和诊断极为有用。

请注意,HTML 表示是最新版本 Sklearn(1.0.0 及以后版本)的默认设置。

6. 管道缓存

一旦你的管道准备好,你可能会想让它全天候运行。然而,由于管道包含多个转换器来处理数据,重新运行相同的操作可能会耗时。

为了解决这个问题,Sklearn 提供了一个 memory 参数,允许你缓存管道内转换器的输出。这个缓存机制有助于避免不必要的重新计算转换器的输出。以下是使用方法:

from shutil import rmtree
from tempfile import mkdtemp

from sklearn.decomposition import PCA

# Make a temporary directory
cache_dir = mkdtemp()

estimators = [("reduce_dim", PCA()), ("clf", LogisticRegression())]
my_pipe = Pipeline(estimators, memory=cache_dir)

# Run the pipeline
...

# Remove the cache directory at the end of your script
rmtree(cache_dir)

要启用缓存,你需要使用 mkdtemp 函数创建一个临时目录。然后,你可以将该目录路径传递给 Pipeline 对象的 memory 参数。

最后,确保在脚本或 notebook 的末尾包含 rmtree(cache_dir) 以删除缓存目录及其内容。

但是,使用缓存有一些注意事项(虽然不严重)。你可以在这里阅读更多关于它们的内容。

7. 在其他对象中

尽管管道包含各种转换器,但归根结底,它仍然是一个估计器:

isinstance(my_pipe, BaseEstimator)
True

这意味着它可以在任何典型的独立估计器可以使用的地方使用。例如,管道通常被插入到交叉验证器中,以保护机器学习模型免受数据泄漏:

from sklearn.model_selection import cross_validate

results = cross_validate(
    estimator=full_pipeline_clf,
    X,
    y,
    cv=5,
    n_jobs=-1,
    scoring=["accuracy", "logloss"],
)

或者用于超参数调优器,例如HalvingGridSearch(出于相同的原因):

from sklearn.model_selection import HalvingGridSearchCV
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.svm import SVC

# Define the pipeline with ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ("numeric", num_pipe, num_cols),
        ("categorical", cat_pipe, cat_cols),
    ]
)

pipe = Pipeline(
    [("preprocessor", preprocessor), ("classifier", SVC())]
)

param_grid = {
    "preprocessor__numeric__with_mean": [True, False],
    "preprocessor__categorical__min_frequency": [2, 4, 6],
    "classifier__C": [0.1, 1, 10],
    "classifier__kernel": ["linear", "rbf"],
}

search = HalvingGridSearchCV(
    pipe, param_grid, cv=5, factor=2, random_state=42
)

在这一点上,我想引起你对参数网格定义的注意。看看它是如何定义的:

param_grid = {
    "preprocessor__numeric__with_mean": [True, False],
    "preprocessor__categorical__min_frequency": [2, 4, 6],
    "classifier__C": [0.1, 1, 10],
    "classifier__kernel": ["linear", "rbf"],
}

StandardScaler的第一个参数with_mean作为嵌套参数的一个例子。它前面有两个说明符:preprocessornumeric,由双下划线分隔。

嵌套参数遵循<step_name>__<parameter>语法。在这种情况下,with_mean是一个变换器的参数,它深度为两层。内部管道的名称是numeric,外部管道的名称是preprocessor,结果是preprocessor__numeric__with_mean

通过使用这种语法编写嵌套参数,你不仅可以优化模型的参数,还可以优化内部变换器本身的参数。

8. 自定义变换器

如果你想对数据进行自定义变换,而这些变换在sklearn.preprocessing模块中没有实现怎么办?你必须放弃 Sklearn 管道及其带来的所有好处吗?

绝对不是!使用FunctionTransformer类,你可以将任何 Python 函数转换为一个可以集成到管道中的变换器。例如,考虑以下函数,它在 DataFrame 的每一行中添加一个表示缺失值数量的列:

def num_missing_row(X: pd.DataFrame, y=None):
    # Calculate some metrics across rows
    num_missing = X.isnull().sum(axis=1)

    # Add the above series as a new feature to the df
    X["num_missing"] = num_missing

    return X

要将其转换为变换器,你只需用FunctionTransformer包装它,并将其传递到管道中:

from sklearn.preprocessing import FunctionTransformer

# Create a custom transformer
custom_transformer = FunctionTransformer(func=num_missing_row)

# Pass it into a pipeline
numeric_pipe = make_pipeline(
    StandardScaler(), customer_transformer, LinearRegression()
)

也有可能简单的函数不足以创建自定义变换。在这种情况下,你可以创建自己的类,继承自TransformerMixin类。我不会在这里详细说明,但我建议你查看我去年写的一篇关于该主题的综合文章:

## 深入指南:为任何数据预处理场景构建自定义 Sklearn 变换器

编辑描述

ibexorigin.medium.com

9. 使用管道进行目标变换

大多数情况下,你管道中的变换器专注于特征数组X。然而,也有一些情况需要对目标数组y进行一些预处理。

回归中的一个常见场景是使目标变量符合正态分布,以提高线性模型的拟合效果。如果你在管道外进行标准化,有可能会导致训练集数据泄漏。

为了解决这个问题并简化流程,Sklearn 提供了TransformedTargetRegressor类。通过这个类,你可以在管道中直接包含目标数组的变换,确保数据完整性并减少冗余代码。

from sklearn.compose import TransformedTargetRegressor
from sklearn.preprocessing import QuantileTransformer

# Define the pipeline for X
transformers = ColumnTransformer(...)
full_pipeline = make_pipeline(transformers, LinearRegression())

# Define the transformer for y
qt = QuantileTransformer(output_distribution="normal")

# Define the final regressor
tt = TransformedTargetRegressor(
    regressor=full_pipeline, transformer=qt
)

tt.fit(X, y)

在定义了一个以回归模型如LinearRegression结尾的管道后,你可以将其传递给TransformedTargetRegressor类的regressor参数。此外,你还需要使用transformer参数指定目标数组y的变换器。

有关这个类及其使用的更多信息,你可以参考Sklearn 文档

结论

我相信这篇文章是我关于 Sklearn 的最详细的一篇,除非你可能还算上这两篇:

## 19 个你本该艰难学习的隐藏 Sklearn 特性

编辑描述

towardsdatascience.com ## 文档中埋藏的 10 个 Sklearn 珍品,等待被发现

编辑描述

towardsdatascience.com

无论如何,Sklearn 管道是我不断回到这个最爱库的主要原因之一。它们为混乱的机器学习工作流带来了和谐,以优雅和高效将原始数据转化为黄金。

使用管道,你可以协调变换器、估算器和列变换器的交响乐,轻松驯服最复杂的数据集。

感谢阅读!

喜欢这篇文章以及它那奇特的写作风格?想象一下,能够访问到数十篇类似的文章,所有文章都由一位才华横溢、迷人风趣的作者撰写(顺便说一句,那就是我 :)。

只需 4.99 美元的会员费,你将不仅可以访问我的故事,还可以获取 Medium 上最佳和最聪明的头脑提供的知识宝库。如果你使用我的推荐链接,你将获得我超级感激的心和虚拟的高五,以支持我的工作。

[## 使用我的推荐链接加入 Medium - Bex T.

获得对我所有⚡优质⚡内容的独家访问权限,畅享 Medium 上的所有内容。通过购买来支持我的工作…

ibexorigin.medium.com

图片由我使用 Midjourney 制作

Sklearn 教程:第 1 模块

原文:towardsdatascience.com/sklearn-tutorial-module-1-f31b3964a3b4

我参加了官方的 sklearn MOOC 教程。以下是我的收获。

Yoann MocquinTowards Data Science Yoann Mocquin

·发表在 Towards Data Science ·9 分钟阅读·2023 年 11 月 22 日

--

在多年使用 Python 科学计算栈(NumPy、Matplotlib、SciPy、Pandas 和 Seaborn)后,我意识到下一步就是 scikit-learn,或称为“sklearn”。

图片由 Thought Catalog 提供,来源于 Unsplash

但为什么选择 sklearn?

在所有机器学习库中,scikit-learn 无疑是最简单、最易于学习的框架。它基于科学计算栈(主要是 NumPy),专注于传统但强大的算法,如线性回归/支持向量机/降维,并提供了许多围绕这些算法构建的工具(如模型评估和选择、超参数优化、数据预处理和特征选择)。

但它的主要优势无疑是其文档和用户指南。你可以仅通过 scikit-learn 网站学到几乎所有的东西,且有大量示例。

请注意,其他流行的框架有 TensorFlow 和 PyTorch,但它们学习曲线更陡峭,并且专注于计算机视觉和神经网络等更复杂的主题。由于这是我第一次真正接触机器学习,我决定从 sklearn 开始。

我几个月前已经开始阅读文档,但由于其庞大感到有些迷茫。虽然文档内容丰富且写得非常好,但我不确定按照页面逐一学习 scikit-learn 是否是最好的方法。

好消息是,激发我进一步学习 scikit-learn 的原因是 scikit-learn 的“官方”MOOC 的推出,这个 MOOC 是由 scikit-learn 的实际团队创建的。

[## 使用 scikit-learn 进行 Python 机器学习

使用 scikit-learn 构建预测模型,并获得对其优缺点的实际理解……

www.fun-mooc.fr

在本系列中,我将尝试总结我从组成 MOOC 的 6 个模块中学到的内容。 这对我来说是一个很好的练习,可以锻炼我的记忆并总结所学内容,同时也是一个很好的入门介绍,如果你想接触 sklearn 的话。

请注意,这个 MOOC 是免费的,所以如果你喜欢下面的内容,绝对应该订阅!请注意,这些帖子是我对 MOOC 的精心整理视角,而 MOOC 本身仅仅是对 scikit-learn 的介绍。

模块 1:机器学习概念

第一个模块专注于介绍以下概念:

  • 将数据拆分为训练集/测试集

  • 列选择器/转换器

  • 模型、管道和带有 .fit().transform.predict().score() 方法的估算器 API

  • 交叉验证

所以我们今天的计划是用文字而不是代码来复习这些概念。如果你想进一步了解,我强烈建议你去阅读文档。

将数据拆分为训练集和测试集

机器学习中的一个重要最佳实践是将数据拆分为训练集和测试集。其思路是,给定固定大小的输入数据,我们将用整个数据的一部分——训练集——来训练模型,并在另一部分——测试集——上测试其性能。

这个方法非常重要,原因有很多:机器学习和模型的全部意义在于能够从新的、未见过的数据中预测输出。如果我们使用全部数据来训练模型,我们别无选择,只能用相同的数据来测试其性能。显然,这似乎是一个有偏的练习:模型当然能够根据已经见过的输入来猜测输出,更何况它还接触过对应的输出。这个概念也被称为 泛化与记忆:我们希望模型能够泛化(为新的输入数据推断输出),而不仅仅是记住它所训练的数据。

另一个原因(实际上,换句话说)是为了避免过拟合。过拟合是机器学习中一个非常重要的概念,会在另一个模块中进一步学习。现在,让我们暂且说,当一个模型对它训练的数据学习得过于精细时,就会发生“过拟合”。拥有一个与训练集不同的测试集,让我们检查模型在训练集和测试集上的性能是否大致相同。

为此,scikit-learn 提供了一个简单但重要的函数 train_test_split

from sklearn.model_selection import train_test_split
data_train, data_test, target_train, target_test = train_test_split(
    data,           # the input array that contains all the input features
    target,         # the target array that contains the truth
    test_size=0.25, # the percentage of data attributed to the test set, defaults to 0.25
    shuffle=True,   # if the data should be randomly split (defaults to True)
    random_state=42 # this is an optional parameter to be able to have reproducible splitting
)

这个思路是使用 data_traintarget_train 来训练我们的模型,并使用 data_testtarget_test 来测试模型在未见数据上的表现。

列转换器/预处理器

通常,原始输入数据格式不是很好,需要进行一些预处理步骤,然后才能进入典型模型。例如,如果输入数据包含一个以字符串形式存储的分类列,而模型只使用数值输入,我们需要将这个字符串列转换为数值列(编码相同的信息),以便模型能够利用该特征的信息。

另一个典型的例子是当几个数值特征具有非常不同的尺度和/或单位时。模型通常会从具有相同尺度的数据中受益——即具有或多或少相同的均值和/或围绕其均值的变化尺度。

一旦应用了这些预处理步骤,转换后的数据就会被送到实际模型中。

为了实现这些预处理步骤,scikit-learn 提供了一些有用的工具。第一个是预处理函数(实际上存储在类中),它们有助于改善每个特征的尺度或将分类特征编码为数值格式。

一旦实例化,这些对象可以用于预处理数据:

from sklearn.preprocessing import StandardScaler, OneHotEncoder

scaler = StandardScaler()
encoder = OneHotEncoder()

# Example of StandardScaler
data = np.arange(100)
scaler.fit(data)                            # scaler 'fits' to the data and stores the mean and variance for later use
scaled_data = scaler.transform(data)        # apply the actual transformation
scaled_data = scaler.fit_transform(data)    # do both at once
other_scaled = scaler.transform(other_data) # note that one can apply the same transformation to other data

# Example of OneHotEncoder
data = [['toto'], ['titi'], ['toto'], ['tata']]
encoder.fit_transform(data).toarray() # fit and transform the column into OneHotEncoded columns
array([[0., 0., 1.],
 [0., 1., 0.],
 [0., 0., 1.],
 [1., 0., 0.]])
# The encoder creates 3 new columns, where the first one corresponds to 'toto', 2nd 'titi' and so on

现在让我们回顾一下ColumnTransformer类:它允许你指定一些列与一些预处理器之间的映射。基本的例子是将数值列映射到标准化器,并将分类列映射到独热编码器。所以假设我们有一组数值列和一组分类列,我们可以这样创建一个新的预处理器对象:

from sklearn.compose import ColumnTransformer

categorical_cols = ['gender', 'country']
numerical_cols = ['age', 'weight', 'height']

preprocessor = ColumnTransformer(
     [ # a list of 3-tuple : (name of the preprocessor, the actual preprocessor, list of columns names to apply on)
         ("onehot_cat", OneHotEncoder(), categorical_cols), 
         ("stdsc_num", StandardScaler(), numerical_cols),
     ],
     remainder=OneHotEncoder(), # the remaining cols that are not identified in cat/num cols will be OneHotEncoded
)

这样,preprocessor现在是一个新的预处理器,就像StandardScaler一样,它具有 fit/transform API。我们可以将这个新的预处理器作为我们模型的全局预处理器,直接应用于整个数据矩阵。注意,make_column_transformer也做了相同的事情,而不需要为每个预处理器指定名称。

但如果我们事先不知道数据矩阵中列的名称怎么办?或者我们不想查看所有列,而是仅根据它们的数据类型进行映射呢?为此,我们可以使用make_column_selector辅助函数,它基本上创建了用于提取整个数据矩阵列的过滤器,以便在ColumnTransformer中映射预处理器:

from sklearn.compose import make_column_selector

num_selector = make_column_selector(dtype_exclude=object) # suppose every non-object dtype is numerical
cat_selector = make_column_selector(dtype_include=object) # suppose every object dtype is categorical

preprocessor = ColumnTransformer(
    [
         ("onehot_cat", OneHotEncoder(), cat_selector), 
         ("stdsc_num", StandardScaler(), num_selector),
    ],
    remainder=OneHotEncoder(), # the remaining cols that are not identified in cat/num cols will be OneHotEncoded
)

通过这种方式,我们创建了几乎相同的ColumnTransformer预处理器对象,但不需要假设列名。

流水线

现在我们拥有了定义流水线的所有工具:流水线基本上是各种处理器的串联。既然我们已经有了一些预处理器,我们只需要添加一个预测模型,比如线性回归或支持向量分类器。

为此,我建议使用Pipeline构造函数:

from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression

pipeline = Pipeline(
    [
        ('std', StandardScaler()),
        ('lin_reg', LinearRegression())
  ]
)

新创建的流水线对象再次暴露了 fit/transform API(就像我们上面看到的预处理器一样,也像 LinearRegression()实例)。我们可以用它来训练和测试我们模型(流水线)在拆分数据上的表现:

pipeline.fit(data_train, target_train) # make the model "learn"
y_predicted = pipeline.transform(data_test) # make a prediction on unseen data
# or directly compute a score to measure the performance on the test set
pipeline.score(data_test, target_test)
# to check the training score, we can use
pipeline.score(data_train, target_train)

交叉验证

我想在这个第一模块中介绍的最终主题是交叉验证。

还记得一开始我们将输入数据拆分成训练集和测试集吗?实际上,这个拆分可能显得有些任意。如果由于运气好或运气不好,数据以某种特定的方式划分,对模型性能有利或不利,会怎么样?

为了规避这个风险,我们可以使用所谓的 交叉验证:其思想是将数据以不同的方式划分,并对每个划分训练和测试模型。模型的总体性能由每个划分性能的平均值给出。例如,第一个划分将使用前 75% 的条目进行训练,最后 25% 的条目进行测试。重新开始,第二个划分将使用 25%-100% 的条目进行训练,前 25% 的条目进行测试。依此类推。对于每个划分,模型都从头开始拟合和测试。

from sklearn.model_selection import cross_validate
cv_results_dict = cross_validate(
    pipeline,   # our model
    data,       # the whole input data
    target,     # the whole target data
    cv=10,      # number of splits (defaults to 5)
    return_estimator=True,   # so we can retrieve each fitted pipeline
    return_train_score=True, # to get the train scores, in addition to the test scores
)

输出是一个字典,其中包含大量信息,包括每个数据划分的测试分数和模型。请注意,有多种数据划分方法,包括随机划分或最常见的 KFold,将数据划分为连续子组。

完整的工作示例

让我们用 iris 玩具数据集回顾一下我们看到的所有内容。

在下面的示例中,我们创建了 5 个基于 5 种分类模型的管道,即逻辑回归、决策树、随机森林、支持向量机和 K 最近邻。这里的想法不是了解每个模型如何工作,而是查看创建包含预处理器和模型的管道的整体过程,以及如何以稳健的方式计算它们的性能。

在这里,我们使用辅助函数 make_pipeline 代替可能显得有点冗长的 Pipeline 构造函数。此外,我们不仅仅指定交叉验证的分割数量,而是明确指定我们希望使用的折叠类型:这里是 10 折随机分割。

import numpy as np
from sklearn.model_selection import cross_validate, KFold
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

# Load the Iris dataset
iris = load_iris()
X, y = iris.data, iris.target

# Create models with StandardScaler in a Pipeline
models = {
    'Logistic Regression': make_pipeline(StandardScaler(), LogisticRegression()),
    'Decision Tree': make_pipeline(StandardScaler(), DecisionTreeClassifier()),
    'Random Forest': make_pipeline(StandardScaler(), RandomForestClassifier()),
    'SVM': make_pipeline(StandardScaler(), SVC()),
    'K-Nearest Neighbors': make_pipeline(StandardScaler(), KNeighborsClassifier())
}

# Perform 10-fold cross-validation for each model
for model_name, model in models.items():
    cv_results = cross_validate(
        model, X, y, 
        cv=KFold(n_splits=10, shuffle=True, random_state=42),
        return_train_score=True
    )

    print(f'Model: {model_name}')
    print('---------------------------')
    print(f'Test Accuracy: {np.mean(cv_results["test_score"]):.4f} ± {np.std(cv_results["test_score"]):.4f}')
    print(f'Train Accuracy: {np.mean(cv_results["train_score"]):.4f} ± {np.std(cv_results["train_score"]):.4f}')
    print('\n')

重点

在这第一篇文章中,我们看到:

  • 这意味着什么 将数据分为训练集和测试集,我们为什么要这样做,以及如何做

  • 如何创建 列变换器/预处理器,用于对输入特征应用转换

  • 管道的概念,意味着将各种步骤(如预处理器和模型)连接在一起,以便从基本工具中创建复杂模型

  • 最终,什么是交叉验证:我们为什么以及如何以一种稳健的方式评估模型性能

现在,请给这篇帖子:

  • 如果只是一般般,给 1 个掌声(一般般!)

  • 如果你觉得写得很清楚,给 10 个掌声(很棒!)

  • 如果它非常清楚和有趣,给 50 个掌声(真是太棒了!)

你可能会喜欢我的一些其他帖子,确保查看它们:

Yoann Mocquin

Yoann Mocquin

科学/数值 Python

查看列表3 个故事讽刺地,一组容器Yoann Mocquin

Yoann Mocquin

数据科学与机器学习

查看列表3 个故事Yoann Mocquin

Yoann Mocquin

时间序列的傅里叶变换

查看列表4 个故事

Sklearn 教程:第二章

原文:towardsdatascience.com/sklearn-tutorial-module-2-0739c44f595a

我参加了官方的 sklearn MOOC 教程。这是我的收获。

Yoann MocquinTowards Data Science Yoann Mocquin

·发表于 Towards Data Science ·14 分钟阅读·2023 年 11 月 25 日

--

在多年使用 Python 科学计算栈(NumPy、Matplotlib、SciPy、Pandas 和 Seaborn)之后,我明显感觉到下一步是 scikit-learn,即“sklearn”。

图片由 Nick Morrison 提供,来自 Unsplash

第二个模块侧重于模型分数的概念,包括测试分数和训练分数。这些分数用于定义过拟合和欠拟合,以及偏差和方差的概念。

我们还将了解如何根据模型的复杂性和输入样本的数量来检查模型的性能。

所有图片由作者提供。

如果你没有看到,我强烈推荐你查看本系列的第一篇文章——这样会更容易跟上:

## Sklearn 教程:第一章

我参加了官方的 sklearn MOOC 教程。这是我的收获。

towardsdatascience.com

分数:训练分数和测试分数

我想讨论的第一个概念是训练分数和测试分数分数是以数字形式表达模型性能的一种方式。为了计算这种性能,我们使用分数函数,该函数聚合了模型预测与实际值之间的“距离”或“误差”。例如:

model = LinearRegressor()
model.fit(X_train, y_train)
y_predicted = model.predict(X_test)
test_score = some_score_function(y_predicted, y_test)

在 sklearn 中,所有模型(也称为估算器)提供了一种更快捷的方式来计算模型的分数:

# the model will computed the predicted y-value from X_test, 
# and compare it to y_test with a score function
test_score = model.score(X_test, y_test)
train_score = model.score(X_train, y_train)

模型的实际评分函数依赖于模型及其设计解决的问题类型。例如,线性回归器使用的是 R²系数(数值回归),而支持向量分类器(分类)则使用准确率,这基本上是正确分类预测的数量。

如果模型的默认评分不符合您的需求,您还可以从 sklearn 的 metrics 导入评分函数。许多评分函数可以用于计算模型的评分,每种都有其优缺点。它们在 sklearn.metrics 模块中可用:

from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
import numpy as np

np.random.seed(42)
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)

# See my previous post for why we split the input data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# create and train a linear regression
lr = LinearRegression()
lr.fit(X_train, y_train)

# Compute predicted values
y_pred = lr.predict(X_test)

print("Model Score (R-squared):",
      lr.score(X_test, y_test)) # use the .score method of the model
print("R-squared Score:", 
      r2_score(y_test, y_pred)) # use the the same function, but from sklearn.metrics

# use other score functions
print("Mean Absolute Error:",
      mean_absolute_error(y_test, y_pred))
print("Mean Squared Error:",
       mean_squared_error(y_test, y_pred))
Model Score (R-squared): 0.8072059636181392
R-squared Score: 0.8072059636181392
Mean Absolute Error: 0.5913425779189776
Mean Squared Error: 0.6536995137170021

所以请记住以下内容:从数据集中,我们创建一个训练集和一个测试集。训练模型后,我们可以计算训练集和测试集上的评分,以估计拟合模型的性能。

给定一个固定的输入数据集,这些分数依赖于模型的选择、该模型的参数(例如多项式拟合的度数)、我们如何划分数据集(哪个样本进入哪个集合)以及评分函数的选择。

引入测试和训练得分很重要,因为这些概念用于检查模型的“拟合状态”——过拟合或欠拟合。

过拟合/欠拟合与训练/测试得分的关系

请记住之前拆分和交叉验证背后的原理:

  • 拆分:可以估计模型的泛化性能。

  • 交叉验证:估计泛化的稳健性,平衡单次拆分中的运气/非运气。

同样,请记住,在交叉验证过程中使用了不同的拆分,但其余的过程是相同的:拆分后,在训练集上训练模型,然后我们可以计算该模型的评分(训练得分和测试得分)。

既然如此,让我们定义什么是过拟合和欠拟合。顾名思义,它们对应于模型相对于相关数据集的对立状态。

我们说一个训练好的模型过拟合,如果它对训练数据学习得太多,从而缺乏泛化能力。当训练得分非常好(模型在训练数据上几乎没有错误)但测试得分很差时,可以看出这一点,因此它在泛化方面表现不佳。这可能发生在模型过于复杂/灵活(例如,非常高的多项式度数)、训练集太小或噪声很大时。在这种情况下,训练集的小变化会导致测试预测的重大变化。

另一方面,如果训练模型只是关注非常一般的全局趋势而忽略细节,模型则会出现欠拟合。当训练分数不够好时就能看出这一点,这意味着模型没有足够的灵活性来学习数据的复杂性。这通常发生在模型不够灵活时(我们也说模型过于“受限”),这可能是模型选择或其参数(例如,将 1 度多项式用于拟合 10 度问题)选择的结果。

我们的工作是找到最佳的中间点,在过拟合和欠拟合之间找到最佳平衡点,通过调整模型——在非常一般的意义上,包括模型选择、预处理选择以及所有相关参数。

总结一下,训练误差、测试误差和模型复杂度之间的关系(在固定输入数据集的情况下):

  • 欠拟合:在非常低的复杂度下,模型会欠拟合训练集(因为它没有足够的灵活性来承受数据中的实际复杂性),导致训练集和测试集上都有误差(训练集和测试集应有更多或更少相同的复杂性/噪声,因为它们来自相同的人群/数据集)。

  • 最佳点:随着模型复杂度的增加,远离严重欠拟合,训练误差和测试误差都会减少。

  • 过拟合:如果复杂度增加过多,训练误差会减少(因为我们给模型更多的灵活性来学习训练集),但测试误差会呈指数增长(因为模型“过度学习”了训练集,因此在测试集的新数据上表现较差)。

让我们看一个快速示例:我们将通过改变多项式拟合的度数来修改模型复杂度。真实模型是 0.5 * X**2 + X + 2,我们尝试了不同的度数:0、1、2、10 和 25。由于模型具有 0、1 和 2 次方,因此我们知道 0 度可能会欠拟合,而 25 度可能会过拟合。

%matplotlib qt
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline

def truth(X):
    return 0.5 * X**2 + X + 2 

X = 6 * np.random.rand(100, 1) - 3
y = truth(X) + np.random.randn(100, 1)*2

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=42)

def fitted_model(degree):
    model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
    model.fit(X_train, y_train)
    return model

fig, axes = plt.subplots(1, 5, sharex=True, sharey=True)
xs = np.linspace(X.min(), X.max())

degrees = [0, 1, 2, 10, 25]

for deg, ax in zip(degrees, axes):

    model = fitted_model(deg)
    y_train_pred = model.predict(X_train)
    y_test_pred = model.predict(X_test)
    y_train_score = model.score(X_train, y_train)
    y_test_score = model.score(X_test, y_test)

    ax.plot(xs, truth(xs), '--', alpha=0.2)
    ax.scatter(X, y, alpha=0.5, color="gray")
    ax.scatter(X_train, y_train_pred, label=f"train set", alpha=0.5)
    ax.scatter(X_test, y_test_pred, label=f"test set", alpha=0.5)
    ax.set_xlabel(f"train={y_train_score:.2f}/test={y_test_score:.2f}")
    ax.legend()
    ax.set_title(f"degree={deg}")
fig.tight_layout()

结果如下所示:

从左到右,复杂度通过度数增加。两个分数都从较低开始,并增加到度数 2/10。之后,测试分数明显下降,表明缺乏泛化能力和过拟合。

在极低复杂度的 0 度时,模型欠拟合。训练和测试分数都很低(对于线性回归来说,最佳分数是 1)。

将度数提高到 1显著改善了两个分数,但我们仍然可以在视觉上看到模型过于简单,无法拟合数据趋势。

度数为 2时,模型似乎接近最佳。分数再次明显提高,我们得到了视觉上的一致性。相比于度数为 5,虽然分数稍微好一些,但我们可以看到一个不匹配的峰值(实际上是过拟合)。可能在另一个划分(如交叉验证)中,度数 5 会有很大不同。

在 25 度时,我们可以看到测试分数明显下降,而训练分数不断提高:这明显是过拟合的信号。此时,我们的模型正在记忆训练集,无法对新数据进行泛化。

这种训练/测试分数与模型复杂性的依赖关系展示了过拟合和欠拟合是如何发生的。我们将在下面的验证曲线中进一步检查这一点。

另一个重要术语是:归纳偏差:这是由模型的选择/类型引入的偏差。它内置于模型本身,而不是超参数或样本数量(与由不同超参数如多项式回归的度数引入的偏差相对)。记住,模型的复杂性取决于模型的类型及其参数。

模型性能与样本数量的关系

虽然在大多数情况下,我们必须处理固定大小的输入数据,但另一种查看模型性能和整体机器学习过程的方法是检查分数如何随样本数据的数量变化。

再次,我们可以分为大致 3 个区域:

  • 当样本数量较少时,训练误差和测试误差都很重要(数据不足,模型无法理解发生了什么,无论其灵活性如何)

  • 随着样本数量的增加,训练误差将增加(因为模型复杂性固定),但测试误差将下降(添加更多样本使得模型能够更好地学习)

  • 如果样本数量大幅增加,训练误差和测试误差将几乎趋于一致:模型达到了其潜力。训练误差停止增加,因为模型自身(它所学习的内容)不会被任何新数据点改变,而测试误差受限于模型复杂性,无法再降低。

在样本数量非常大的情况下,我们说模型接近于贝叶斯误差率:这是在无限数据上训练的最佳模型的误差,当预测仅受数据中的噪声限制时。

将分数可视化为复杂性和样本数量的函数

最后,使用所谓的“验证曲线”和“学习曲线”来可视化这些概念是一个不错的实践:

  • 验证曲线:将测试分数和训练分数绘制为模型复杂性的函数(如多项式拟合的度数):score=f(complexity)

  • 学习曲线:将测试分数和训练分数绘制为输入大小的函数(对于给定的输入数据矩阵,我们可以只使用总数据的一部分):score=f(#samples)

两条曲线可以通过 sklearn 简单生成:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split, validation_curve
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import ValidationCurveDisplay

def truth(X):
    return 0.5 * X**2 + X + 2 

X = 6 * np.random.rand(100, 1) - 3
y = truth(X) + np.random.randn(100, 1) * 2
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=42)
degrees = np.arange(0, 12, 1)

# Plotting the validation curve
ValidationCurveDisplay.from_estimator(
    make_pipeline(PolynomialFeatures(), LinearRegression()),
    X, y,
    param_name='polynomialfeatures__degree',
    param_range=degrees,
)
plt.xlabel('Degree of Polynomial Features')
plt.ylabel('Score')
plt.title('Validation Curve for polynomial fit')

正如你所见,训练分数和测试分数在[0–2]范围内快速增加。对于更高的度数,测试分数开始下降,表明模型的泛化能力下降。显然,随着模型复杂性的增加,训练分数持续增加。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import ValidationCurveDisplay, LearningCurveDisplay

def truth(X):
    return 0.5 * X**2 + X + 2 

N = 1000
X = 6 * np.random.rand(N, 1) - 3
y = truth(X) + np.random.randn(N, 1) * 2

LearningCurveDisplay.from_estimator(
    make_pipeline(PolynomialFeatures(2), LinearRegression()),
    X, y,
    train_sizes=np.logspace(-2, 0, 20), 
)
plt.xlabel('# of samples')
plt.ylabel('Score')
plt.title('Learning Curve for polynomial fit')

如你所见,固定度数为 2 时,训练分数和测试分数在样本数量非常多时趋于相同的值。这种曲线可以帮助你分析你所处理的输入数据是否足够让你的模型接近其贝叶斯误差率。

偏差-方差权衡

偏差、方差和偏差-方差权衡的概念与过拟合和欠拟合的概念密切相关。我们已经讨论了过拟合和欠拟合,所以我会简要说明:

  • 方差 指的是模型响应在训练集中的变化程度。换句话说,当训练集非常小和/或模型复杂性非常高时,模型可能会表现出强烈的方差。过拟合通常与高方差相关,因为模型对训练数据的具体情况非常敏感。

  • 偏差 指的是拟合模型相对于完美模型的“偏倚”,无论输入什么内容,结果都大致相同。这尤其发生在模型复杂性/灵活性与我们希望它学习的内容相比非常低时。换句话说,模型对其假设有偏见,可能无法很好地适应数据的复杂性。欠拟合与高偏差相关,因为模型的灵活性不足以适应数据的复杂性。

偏差-方差权衡 是机器学习中的一个关键概念:它表明偏差和方差之间存在权衡。增加模型复杂性通常会减少偏差,但增加方差,反之亦然。目标是找到一个合适的平衡点,以最小化偏差和方差,从而得到一个能很好地泛化到新的、未见过的数据的模型。因此,我们作为数据科学家的工作是调整模型并找到这个甜蜜点。

下面的示例展示了一个单变量的简单多项式拟合,即 y=p(x),它可以具有高偏差或高方差,这取决于模型允许的度数(代码在下方的图形下面提供)。

思路如下:我们创建一个玩具数据集 y,其已知多项式函数为 p(x)=0.5X³ + X + 2 + 噪声。因此,我们希望多项式拟合学习到的真实系数是常数项为 2,X 项为 1,以及 X³ 项为 0.5。我们测试 2 个模型:一个度数为 1 的低度多项式和一个度数为 15 的高度多项式。

一旦生成数据,我们将其分成 50–50 的训练集/测试集。低度和高度模型都在这个划分上进行拟合,并计算它们的学习系数、预测和评分。然后我们交换训练集和测试集,即第二次划分时,我们在原测试集上训练,在原训练集上测试。这不是常见的做法,而是一种教学技巧。再次,低度和高度模型都进行拟合,并计算它们的学习系数、预测和评分。

结果如下:

正如你可以看到低度数的图(中间列的图),两个拆分得出的系数解几乎是一样的。预测与真实数据有“偏差”,学习到的系数存在这种常量“偏移”。

另一方面,对于高阶图(右列图),两个拆分得到的系数差异很大。它们的响应具有高方差。

这是使用的代码:

%matplotlib qt
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

degree_low = 2
degree_high = 15
true_coefs = np.zeros(degree_high)

true_coefs[4] = 0.5
true_coefs[1] = 1
true_coefs[0] = 2

N = 100
X = 6 * np.random.rand(N, 1) - 3
y = 0.5 * X**3 + X + 2 + np.random.randn(N, 1)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=42)

def compute_results(X_train, X_test, y_train):
    model_underfit = make_pipeline(PolynomialFeatures(degree=degree_low), LinearRegression())
    model_underfit.fit(X_train, y_train)
    y_pred_train_underfit = model_underfit.predict(X_train)
    y_pred_test_underfit = model_underfit.predict(X_test)
    model_overfit = make_pipeline(PolynomialFeatures(degree=degree_high), LinearRegression())
    model_overfit.fit(X_train, y_train)
    y_pred_train_overfit = model_overfit.predict(X_train)
    y_pred_test_overfit = model_overfit.predict(X_test)
    return model_underfit, model_overfit, y_pred_train_underfit, y_pred_test_underfit, y_pred_train_overfit, y_pred_test_overfit

# Plot the results
fig, axes = plt.subplots(3, 3, figsize=(12, 8))
axes[0,0].set_title("RAW input data")
axes[0,0].set_ylabel('First split')
axes[2,0].set_ylabel('Second split')
axes[1,0].set_ylabel('Poly. coefs.')
axes[0,0].set_title("Input data")
axes[0,1].set_title("Low-degree, biased model (degree=1)")
axes[0,2].set_title("High-degree, high variance (degree=15)")
axes[1,0].bar(np.arange(len(true_coefs)), true_coefs, alpha=0.3, label="True coefs")

model_underfit, model_overfit, y_pred_train_underfit, y_pred_test_underfit, y_pred_train_overfit, y_pred_test_overfit = compute_results(X_train, X_test, y_train)

axes[0,0].scatter(X_train, y_train, label='Training Data', color='red', alpha=0.7)
axes[0,0].scatter(X_test,  y_test, label='Test Data', color='blue', alpha=0.7)
axes[0,1].scatter(X, y, color="gray", alpha=0.2)
axes[0,1].scatter(X_train, y_pred_train_underfit, color='red', label='Train', alpha=0.7)
axes[0,1].scatter(X_test,  y_pred_test_underfit, color='blue', label='Test', alpha=0.7)
axes[0,2].scatter(X, y, color="gray", alpha=0.2)
axes[0,2].scatter(X_train, y_pred_train_overfit, color='red', label='Train', alpha=0.7)
axes[0,2].scatter(X_test,  y_pred_test_overfit, color='blue', label='Test', alpha=0.7)
axes[1,1].bar(np.arange(degree_low+1), model_underfit.named_steps['linearregression'].coef_.flatten(),  label="1st split", alpha=0.3)
axes[1,2].bar(np.arange(degree_high+1), model_overfit.named_steps['linearregression'].coef_.flatten(),  label="1st split", alpha=0.3)
axes[1,0].set_xticks(np.arange(len(true_coefs))); axes[1,0].set_xlim(-1, 15); axes[1,0].set_ylim(-5, 5)
axes[1,1].set_xticks(np.arange(len(true_coefs))); axes[1,1].set_xlim(-1, 15); axes[1,1].set_ylim(-5, 5)
axes[1,2].set_xticks(np.arange(len(true_coefs))); axes[1,2].set_xlim(-1, 15); axes[1,2].set_ylim(-5, 5)
axes[0,1].set_xlabel(f'Train score={model_underfit.score(X_train, y_train):.2f} / Test score={model_underfit.score(X_test, y_test):.2f}')
axes[0,2].set_xlabel(f'Train score={model_underfit.score(X_train, y_train):.2f} / Test score={model_underfit.score(X_test, y_test):.2f}')

# Switch train and test sets...
X_train, X_test, y_train, y_test = X_test, X_train, y_test, y_train 

# ... and start over
model_underfit, model_overfit, y_pred_train_underfit, y_pred_test_underfit, y_pred_train_overfit, y_pred_test_overfit = compute_results(X_train, X_test, y_train)

axes[2,0].scatter(X_train, y_train, label='Training Data', color='red', alpha=0.7)
axes[2,0].scatter(X_test,  y_test, label='Test Data', color='blue', alpha=0.7)
axes[2,1].scatter(X, y, color="gray", alpha=0.2)
axes[2,1].scatter(X_train, y_pred_train_underfit,  color='red', label='Train', alpha=0.7)
axes[2,1].scatter(X_test,  y_pred_test_underfit, color='blue', label='Test', alpha=0.7)
axes[2,2].scatter(X, y, color="gray", alpha=0.2)
axes[2,2].scatter(X_train, y_pred_train_overfit, color='red', label='Train', alpha=0.7)
axes[2,2].scatter(X_test,  y_pred_test_overfit, color='blue', label='Test', alpha=0.7)
axes[1,1].bar(np.arange(degree_low+1), model_underfit.named_steps['linearregression'].coef_.flatten(),  label="2nd split", alpha=0.3)
axes[1,2].bar(np.arange(degree_high+1), model_overfit.named_steps['linearregression'].coef_.flatten(),  label="2nd split", alpha=0.3)
axes[1,0].set_xticks(np.arange(len(true_coefs))); axes[1,0].set_xlim(-1, 15); axes[1,0].set_ylim(-5, 5)
axes[1,1].set_xticks(np.arange(len(true_coefs))); axes[1,1].set_xlim(-1, 15); axes[1,1].set_ylim(-5, 5)
axes[1,2].set_xticks(np.arange(len(true_coefs))); axes[1,2].set_xlim(-1, 15); axes[1,2].set_ylim(-5, 5)
axes[2,1].set_xlabel(f'Train score={model_underfit.score(X_train, y_train):.2f} / Test score={model_underfit.score(X_test, y_test):.2f}')
axes[2,2].set_xlabel(f'Train score={model_underfit.score(X_train, y_train):.2f} / Test score={model_underfit.score(X_test, y_test):.2f}')

fig.suptitle('Comparison for 2 splits of low/high degree polynomial models')
for ax in axes.flatten(): ax.legend()

plt.tight_layout()

总结

总结一下,记住这些重要概念:

  • 分数/训练分数/测试分数: 分数量化了模型的表现;训练分数反映了模型在训练数据上的准确性,测试分数则是在未见数据上的准确性。平衡高训练分数与可比较的测试分数对一个能够良好泛化的模型至关重要。

  • 欠拟合/过拟合: 欠拟合发生在模型过于简单,未能捕捉数据复杂性;过拟合发生在模型过度适应训练数据,阻碍泛化。找到模型复杂度的平衡对避免欠拟合的简单性和过拟合的记忆化至关重要。

  • 模型性能作为复杂性和样本数的函数: 检查模型分数如何随复杂性(例如,多项式度数)或数据集大小的变化而变化,可以提供有关其行为的见解。在不同复杂性和样本大小下评估性能有助于识别最佳模型特征。

  • 模型评估中的偏差和方差: 偏差指的是模型偏离真实数据模式的倾向;低灵活性会产生高偏差。方差则捕捉了模型对数据集变化的敏感性;高复杂性导致高方差。

最后,实现偏差-方差平衡通常是调整模型以获得最佳性能的关键点。

你可能会喜欢我的其他一些帖子,确保查看一下:

Yoann Mocquin

Yoann Mocquin

Sklearn 教程

查看列表9 个故事Yoann Mocquin

Yoann Mocquin

科学/数值 Python

查看列表3 个故事讽刺地,容器数组Yoann Mocquin

Yoann Mocquin

数据科学与机器学习

查看列表3 篇故事Yoann Mocquin

Yoann Mocquin

时间序列的傅里叶变换

查看列表4 篇故事

Sklearn 教程:模块 3

原文:towardsdatascience.com/sklearn-tutorial-module-3-08c9ae5cb8fa

我参加了官方的 sklearn MOOC 教程。以下是我的收获。

Yoann MocquinTowards Data Science Yoann Mocquin

·发表于 Towards Data Science ·9 分钟阅读·2023 年 12 月 1 日

--

这是我 scikit-learn 教程系列中的第三篇文章。如果你错过了,我强烈推荐你阅读前两篇文章——这样会更容易跟上:

## Sklearn 教程:模块 1

我参加了官方的 sklearn MOOC 教程。以下是我的收获。

towardsdatascience.com ## Sklearn 教程:模块 2

我参加了官方的 sklearn MOOC 教程。以下是我的收获。

towardsdatascience.com

在第三模块中,我们将了解什么是超参数,以及我们为什么和如何优化它们。

图片由 Glenn Carstens-Peters 提供,来自 Unsplash

什么是超参数

在设置模型时,我们只改变了预处理、模型类型或两者——但我们还没有真正调整模型的超参数。

模型的超参数是我们数据科学家在创建模型/管道时设定的参数。它们是在模型看到任何数据之前定义模型的参数。可以说,它们允许我们定义相同管道的不同“变体”。

超参数通常会影响模型的复杂性,从而影响学习过程和整体模型性能。给定数据集和你想解决的问题,作为数据科学家的你需要在“超参数化模型”的无限空间中找到最佳的“超参数化模型”。

超参数不应与模型在学习过程中学到的内部参数混淆——这些内部参数也称为“系数”。例如,在多项式回归中,超参数(在学习之前设置)是回归的度数,而使用训练集学到的内部参数是多项式系数(a/b/c 在 aX² + bX + c 中)。换句话说,你首先设置度数(超参数),然后使用数据进行回归拟合(内部系数被学习)——而不是反过来。

因此,模型超参数可以在模型/预处理器创建时设置。例如,在 scikit-learn 中:

  • PolynomialFeatures(degree=degree):根据每个特征创建的多项式度数。

  • Ridge(alpha=5):线性岭回归的正则化项。

  • SVC(C=1.0, kernel="rbf"):用于支持向量分类器的正则化参数和核函数。根据选择的核函数,还可以使用其他超参数。

  • KNeighborsClassifier(n_neighbors=5):在 K 近邻分类器中考虑的邻居数量。

  • StandardScaler(with_mean=True, with_std=True):标准化器预处理器也可以通过其超参数进行调节,比如是否去除均值和/或除以标准差。

这些例子表明,可用的超参数取决于你为模型使用的整个管道。例如,以下管道具有标准化器和回归器的超参数:

from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression
from sklearn.preprocessor import StandardScaler

pipeline = Pipeline(
    steps=[
    "standard_scaler", StandardScaler(with_mean=True), # has with_mean/with_std hyperparameters
    "linear_regression", LinearRegression(fit_intercept=True), # has fit_intercept 
 ]
)
# This pipeline's hyperparameters set is the union of the hyperparameters of each step of the pipeline

如下所示,超参数还可以在管道创建后进行读取和设置。我们甚至会看到个别步骤可以被视为超参数(例如,标准化器预处理器的“类型”,其可能的值有“StandardScaler”、“MinMaxScaler”等)。

请注意,对于给定的数据集,就像某种模型可能优于另一种模型一样——某个超参数也可能优于另一个。换句话说,对于每个数据集,都存在一个最佳的超参数集。

所以请记住:

  • 超参数对应于在创建模型时设置的参数,在模型接收数据集之前。

  • 它们对应于你在创建管道时可以设置的每个参数,具体取决于管道中的每一步。

  • 最佳超参数集取决于 ML 任务的目标和输入数据集。

  • 我们的工作是找到最佳的超参数。

本文其余部分解释了如何访问和修改模型的超参数,以及搜索和优化这些超参数的不同方法。

如何获取/设置管道/模型的超参数:

在 sklearn 中,一旦创建了模型或管道,就可以使用 API 来:

  • 列出可用的超参数及其各自的值。

  • 更改其值。

对于给定的模型,你可以使用**.get_params()** 方法获取所有超参数及其值

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
pipeline = Pipeline(
    steps=[
        ('preprocessor', StandardScaler()),
        ('lin_reg', LinearRegression())
    ]
)
pipeline.get_params()
{
 'memory': None,
 'steps': [
 ('preprocessor', StandardScaler()),
 ('lin_reg', LinearRegression())
 ],
 'verbose': False,
 'preprocessor': StandardScaler(),
 'lin_reg': LinearRegression(),
 'preprocessor__copy': True,
 'preprocessor__with_mean': True,
 'preprocessor__with_std': True,
 'lin_reg__copy_X': True,
 'lin_reg__fit_intercept': True,
 'lin_reg__n_jobs': None,
 'lin_reg__positive': False
}

几个重要的事项需要注意:

  • .get_params() 返回一个字典,包括一个 steps 条目,该条目包含管道步骤的列表

  • 创建管道时使用的名称,preprocecssorlin_reg 在我们的例子中,被用于和存储在这个参数字典中

  • 因此,每个步骤的超参数都使用<step_name>__<parameter_name>的约定命名,步骤名称和步骤参数名称之间用双下划线分隔

为了使 API 接口一致,请注意所有参数都在这个字典中返回,包括一些对性能没有影响的超参数(如lin_reg__n_jobspreprocessor__copy)。

类似地,我们可以使用以下一致的 API 来更改这些参数中的任何一个值 **set_params(name=value)**:

# to change 2 parameters at once
pipeline.set_params(lin_reg__fit_intercept=False, preprocessor__with_std=False)
# to change the scaler step completly
pipeline.set_params(preprocessor=MinMaxScaler())
pipeline.get_params()
{
 'memory': None,
 'steps': [
     ('preprocessor', MinMaxScaler()), 
     ('lin_reg', LinearRegression())
 ],
 'verbose': False,
 'preprocessor': MinMaxScaler(),
 'lin_reg': LinearRegression(),
 'preprocessor__clip': False,
 'preprocessor__copy': True,
 'preprocessor__feature_range': (0, 1),
 'lin_reg__copy_X': True,
 'lin_reg__fit_intercept': True,
 'lin_reg__n_jobs': None,
 'lin_reg__positive': False
}

如前所述,我们甚至可以使用相同的 API 完全更改一个步骤:这里我们将预处理器从 StandardScaler 更改为 MinMaxScaler。注意在更改预处理器类型后,参数的可用差异(仍称为‘preprocessor’,但相应的超参数是 MinMaxScaler 的那些)。

手动超参数调优

现在我们知道了什么是超参数,如何获取/设置它们,以及为什么我们应该优化它们,让我们首先采取一种方法来进行优化。

对于任何优化问题,我们识别:

  • 我们想要探索的“空间”:这就是我们想要尝试的所有超参数值。

  • 我们想要优化的值:这里对应于模型的性能得分。

最简单且最无效、非鲁棒的优化方式是对一个超参数使用循环,并使用单次训练/测试划分的得分:

pipeline = Pipeline(
     [('preprocessor', StandardScaler()),
      ('lin_reg', LinearRegression())]
)
X_train, X_test, y_train, y_test = train_test_split(X, y)
for with_mean in [True, False]:
    pipeline.set_params(preprocessor__with_mean=with_mean)
    pipeline.fit(X_train, y_train)
    print(f"with_mean={with_mean}: score={pipeline.score(X_test, y_test)}")
# we can then identify the best value for with_mean

所以在这种初步方法中,我们手动编写了一个循环,其中管道被拟合和测试。我们可以做的第一个改进是使用交叉验证来计算一个更有意义的得分:

for with_mean in [True, False]:
    pipeline.set_params(preprocessor__with_mean=with_mean)
    cv_results = cross_validation(pipeline, X, y)
    print(f"with_mean={with_mean}: score={cv_results['test_score']}")
# we can then identify the best value for with_mean, with more certainty about our choice

使用交叉验证,我们对每个超参数值都有一个更稳健的模型性能估计。

现在,让我们通过优化两个超参数进一步改进:我们需要嵌套两个循环,每个超参数一个:

for with_mean in [True, False]:
    for with_std in [True, False]:
        pipeline.set_params(preprocessor__with_mean=with_mean, preprocessor__with_std=with_std)
        cv_results = cross_validation(pipeline, X, y)
        print(f"with_mean={with_mean}/with_std={with_std}: score={cv_results['test_score']}")
# we can then identify the best value for (with_mean, with_std)

现在,如果我们想优化 3、4、10 或更多的超参数呢?如果我们想对每个超参数尝试 10 种不同的值呢?我们必须编写许多嵌套循环并检查许多得分。

这就是为什么 scikit-learn 提供了自动化这个超参数搜索过程的辅助函数,如 GridSearchCV 和 RandomSearchCV。

使用 GridSearch 进行自动调优

sklearn 提供的第一个自动化方法来优化超参数被称为GridSearchCV这个方法的核心思想是使用字典来指定每个超参数的所有值,并测试所有组合。例如,为了重现上述示例,其中with_mean可以是[True, False]with_std可以是[True, False],我们会使用:

param_grid = {
    "preprocessor__with_mean":[True, False],
    "preprocessor__with_std":[True, False],
}
model_grid_search = GridSearchCV(pipeline, param_grid=param_grid)

这个第一个代码片段仅创建了一个model:是的,一个新的模型,它封装了真正的低级管道。这个新的网格搜索模型可以拟合,再次使用model_grid_search.fit。在这个拟合步骤中,所有超参数组合都会被测试,并使用交叉验证计算模型性能。一旦网格搜索拟合完成,它可以像其他预测器一样使用(例如调用 predict 或 score),使用在拟合过程中找到的最佳参数的模型:

# fit the gridsearch model
model_grid_search.fit(X_train, y_train)
# use the best model found
model_grid_search.score(X_test, y_test)
model_grid_search.predict(X_new)
# or inspect the results of the grid search
model_grid_search.cv_results_

换句话说,拟合一个 GridSearch 模型意味着尝试所有组合并保留最佳的一个。

一个重要的特点是,我们可以使用字典列表而不仅仅是单个字典来指定我们想尝试的组合,以便精细调整应测试的超参数集合。例如:

param_grid = [
    {
        "preprocessor":StandardScaler(), 
        "preprocessor__with_mean":[True, False], 
        "preprocessor__with_std":[True, False],
    },
    {
        "preprocessor":MinMaxScaler(), 
        "preprocessor__feature_range":[(0, 1), (0, 0.5), (0.25, 0.75)],
    },
]
# This grid search will try the StandardScaler with all combinations of with_mean/with_std AND the MinMaxScaler with 3 different ranges
model_grid_search = GridSearchCV(pipeline, param_grid=param_grid)

使用 RandomizedSearchCV 进行随机调优

当超参数是连续值且范围很大,和/或需要调整的超参数数量很重要,和/或模型计算复杂时,GridSearchCV 的全组合方法显现出它的局限性:拟合时间开始增加。显然,测试超参数集合的数量与总时间之间存在权衡。

为了规避这些限制并提高找到一个好的——如果不是最好的——超参数集合的机会,我们可以使用随机方法来抽样超参数空间。

这个想法是指定所有超参数的所有可能值,并随机尝试集合。

使用随机方法来优化数值问题是一种常见的技巧,例如用于数值积分或优化问题。

为了在 sklearn 中实现这一点,我们使用RandomizedSearchCV —— 使用方法与GridSearchCV完全相同。例如,假设我们想通过调整其 C 参数(可以是从 0 到无穷大的任何值)以及其他超参数如 kernel 和 gamma 来优化支持向量分类器:

from sklearn.model_selection import RandomizedSearchCV
param_grid = {
    'C': uniform(0,1000).rvs(100),
    'kernel': ['linear', 'rbf', 'poly', 'sigmoid'],
    'gamma': ['scale', 'auto'] + list(uniform(0,1).rvs(10))
}
random_search_model = RandomizedSearchCV(pipeline, param_grid=param_grid, n_iter=1000)
# fit the gridsearch model
random_search_model.fit(X_train, y_train)
# use the best model found
random_search_model.score(X_test, y_test)
random_search_model.predict(X_new)
# or inspect the results of the grid search
random_search_model.cv_results_

在这里,我们允许搜索尝试 1000 个超参数集合,使用n_iter来控制尝试次数。

所以记住:随机化方法允许随机尝试超参数,并使用 n_iter 控制尝试次数。当一些超参数是连续值且/或可能取一个广泛的值范围时,这种方法是有用的。

嵌套交叉验证模式

为了训练和找到使用GridSearchCV/RandomizedSearchCV发现的最佳超参数模型,我们使用第一次拆分的原始训练集。这个第一次拆分的训练集在内部使用了另一个训练/测试拆分。换句话说:

  • 第一次拆分:原始数据集被拆分成 X_train 和 X_test。

  • 然后 X_train 被用来通过训练/测试每个超参数集合,使用 N 个内部拆分(折数)来优化超参数:所以 X_train 被拆分 n 次成另一个 X_train/X_test。模型对于每个超参数集合进行拟合/测试,并使用交叉验证评估模型性能。

  • 最终,找到的最佳模型在原始 X_test 集上进行测试和评估。

这意味着这种方法仅为我们提供了对泛化性能的单次评估,因为仅保留了原始 X_test 集,且在学习步骤(拟合和优化)中从未使用。为了改善对泛化性能的估计,我们可以使用外部交叉验证循环。

所以请记住:外部循环用于估计整体拟合/优化过程的泛化性能。换句话说,估计的最佳模型性能是通过交叉验证来评估的。

# nested-cross validation pattern: 
cv_results = cross_validate(
    model_grid_search, X, y,
)

这样,我们可以在增加计算量的代价下获得两全其美的效果。

总结

第三个模块专注于超参数:

  • 超参数是定义模型工作和学习方式的参数,它们定义了模型的复杂性。它们不应与模型在展示训练集时学习到的内部系数混淆。

  • 由于这些超参数对模型有很大影响,因此必须对其进行优化,以提高模型在给定任务上的表现。最佳超参数取决于输入数据。

  • 优化超参数可以通过交叉验证搜索方法,如网格搜索和随机搜索来完成。

  • 在优化超参数时,一个好的做法是使用嵌套交叉验证模式来估计最佳拟合模型的性能。

你可能会喜欢我其他的一些帖子,确保查看一下:

Yoann Mocquin

Yoann Mocquin

Sklearn 教程

查看列表9 个故事Yoann Mocquin

Yoann Mocquin

科学/数值 Python

查看列表3 个故事具有讽刺意味的容器数组Yoann Mocquin

Yoann Mocquin

数据科学与机器学习

查看列表3 篇故事Yoann Mocquin

Yoann Mocquin

时间序列的傅里叶变换

查看列表4 篇故事

Sklearn 教程:模块 4

原文:towardsdatascience.com/sklearn-tutorial-module-4-1e1a50e5247d

线性模型、处理非线性和正则化

Yoann MocquinTowards Data Science Yoann Mocquin

·发布在 Towards Data Science ·14 分钟阅读·2023 年 12 月 22 日

--

这是我 scikit-learn 教程系列的第四篇文章。如果你错过了前几篇,我强烈推荐你先阅读前三篇——这样会更容易跟上:

Yoann Mocquin

Yoann Mocquin

Sklearn 教程

查看列表9 个故事

第 4 模块介绍了线性模型的概念,使用臭名昭著的线性回归逻辑回归模型作为实际示例。

除了这些基本的线性模型外,我们展示了如何利用特征工程来仅使用线性模型处理非线性问题,以及正则化的概念以防止过拟合。

总体而言,这些概念使我们能够创建非常简单但强大的模型,能够处理许多机器学习问题,通过微调超参数来避免过拟合,同时处理非线性问题。

图片由 Roman Synkevych 拍摄,来自 Unsplash

所有图表和图像均由作者制作。

线性模型

线性模型是通过设置系数来“拟合”或“学习”的模型,使其最终仅依赖于输入特征的线性组合。 换句话说,如果输入数据由 N 个特征 f_1 到 f_N 组成,则模型在某些时候基于线性组合:

模型学习到的系数是 N+1 个系数 beta。系数 beta_0 代表一个偏移量,无论输入值如何,输出中都存在一个常量值。这种模型的思想是“真实情况”可以通过输入和输出之间的线性关系来近似。

在回归问题中,我们想从输入中预测一个数值,其中一个最简单且广为人知的线性模型是线性回归。你很可能已经进行过数百次线性回归(通过手动操作、在 Excel 中或使用 Python)。

在分类问题中,我们想要从输入中预测一个类别,最简单且广为人知的线性模型是逻辑回归(不要被“逻辑回归”中的“回归”欺骗,它实际上处理的是分类问题)。

还有许多其他线性模型,例如支持向量回归和支持向量分类,以及许多线性回归和逻辑回归的变体。这些都可以成为一系列文章的主题。这里的想法不是深入审查它们,而是展示它们的基本使用和限制(尽管我的完备症会让我详细讲解一些)。

重要说明: sklearn 的一个承诺立场是提供开箱即用的模型,以便新手可以快速运行代码(而不需要花费太多时间设置框架的花招或在设置新模型时处理很多错误)——我仍然建议广泛阅读文档,因为它写得很好,你将会学到很多关于 Python API 以及数学和良好实践的知识。

回归的线性模型:线性回归

线性回归是最著名的线性回归模型。如上所述,其思想是通过输入 f_i 的线性组合尽可能准确地近似输出 y:

线性回归和线性模型广泛流行的原因之一是它们可以用矩阵来处理,因为矩阵是线性操作的表示。特别是,学习系数 beta 的一种可能方法(在科学中普遍使用,尤其是在机器学习中)是使用普通最小二乘法。

普通最小二乘法 旨在选择一个最佳的 beta 向量,使得平方误差之和最小:这种方法的优点在于易于解释(模型“最小化”数据与预测之间的平方距离),并且它有一个封闭形式的解(因此不需要数值优化方法,它基本上只是矩阵乘法和逆运算)。如果你查看 sklearn 的 LinearRegression 文档,你会看到正是这个方法被实现了。

还有许多其他方法可以用来拟合线性回归,这些方法都导致了该最简单模型的“变体”。正如我们下面将看到的,岭回归和套索回归是这些变体的一部分。

注意,线性回归允许进行多项式回归,只需应用一个小的预处理步骤。事实上,多项式就像是输入的单项式的线性组合:

所以一个多项式可以写成

在单变量(单一输入特征)的情况下。为此,我们只需生成一个新的输入矩阵,该矩阵由我们想要的所有多项式变量组成(对于单变量问题,是 X¹、X² 等,甚至在多变量多项式情况下如 X_1 X_2、X_1 X_3 等的交叉变量)。

这是一个关于 1d 输入特征的线性回归简单示例,因此模型实际上只学习 beta0 和 beta1:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression

X = 2 * np.random.rand(100, 1)
y_true = 4 + 3 * X
y = y_true + 0.5 * np.random.randn(100, 1)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

fig, ax = plt.subplots()
ax.scatter(X_train, y_train, alpha=0.5, label='Training Set', color='blue')
ax.scatter(X_test, y_test, alpha=0.5,label='Test Set', color='green')
ax.plot(X, y_true, label='True Underlying Model', color='red', linestyle='--')
ax.plot(X_test, y_pred, label='Linear Regression Model', color='orange')
ax.set_xlabel('X')
ax.set_ylabel('y')
ax.set_title(f'Linear Regression on a 1D feature with test score R²=\n {model.score(X_test, y_test):.2f}')
ax.legend()

让你多了解一些关于 sklearn 中线性回归模型的信息:

  • 你可以通过设置 fit_intercept 超参数 LinearRegression(fit_intercept=True) 来调整线性回归是否处理偏置 beta0。如果使用 False,则模型期望目标 y 被中心化,即均值为 0。

  • 一旦模型拟合完成,它就会学习到系数 beta。你可以使用 model.intercept_model.coefficients_ 来检查这些系数。请记住,在 sklearn API 中,学习到的参数后缀带有下划线“_”。

  • 线性回归的默认评分是 R² 系数,它表明拟合模型“解释”数据集变异性的程度。当然,你也可以从 metrics 模块中导入任何评分函数,并使用例如 from sklearn.metric import mean_absolute_error; mean_absolute_error(y_true, y_pred) 来计算其他评分。

分类的线性模型:逻辑回归

线性回归在分类问题中的等价物是逻辑回归。

这个想法很简单:创建一个线性组合 y,当其输入到逻辑函数时,能够最好地分隔目标类别。像线性回归一样,线性组合 y 可以具有任何值——但为了满足分类的背景,它被输入到逻辑函数中,该函数是一个 S 形函数,接受任何实数输入,并将其映射到 [0, 1] 区间。这个区间然后与目标类别相关联,其中 0 对应于一个类别,+1 对应于另一个类别。

换句话说,如果一个样本从线性组合中映射到一个非常负的值,它将与类别 0 密切相关。随着 y 值的增加并接近 0.5,目标类别变得“不确定”。然后,如果线性组合 y 继续增加超过 0.5,它将被映射到类别+1。在这种情况下,我们称 0.5 为分类阈值。请注意,一些其他类似的算法使用的是[-1,1]的映射区间,阈值为 0。这些基本上只是惯例,不会改变模型的性能。

所以我们可以这样写模型:

其中 x 代表一个长度为 N 的样本向量,特征为 f_1 到 f_N,y 是该样本与模型系数的线性组合,系数可以有任意值,该值通过逻辑函数映射到[0-1]区间。

换句话说,一个样本属于某一类别的概率与其对应的线性组合值 y 有关。最终的类别是基于其相对于阈值的位置,最接近的 - 或最可能的 - 类别。

在 sklearn 术语中,概率是通过.predict_proba计算的,该方法返回一个浮点数组,数组的和为 1,以表示属于某个类别的概率。另一方面,.predict返回一个类别,对应于.predict_proba的最可能类别。

让我们看一个简单的一维例子:同样,线性模型只有一个输入,因此 X 轴可以用来绘制特征值或 y 线性组合(beta1X+beta0):

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=100, n_features=1, n_informative=1, n_redundant=0, n_clusters_per_class=1)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LogisticRegression()
model.fit(X_train, y_train)

x_ = np.linspace(-2, 3).reshape(-1,1)

fig, ax = plt.subplots()
ax.scatter(X_train[y_train == 0], y_train[y_train == 0], label='Class 0 (Training)', color='blue')
ax.scatter(X_train[y_train == 1], y_train[y_train == 1], label='Class 1 (Training)', color='red')
ax.scatter(X_test[y_test == 0], y_test[y_test == 0], label='Class 0 (Test)', marker='s', color='blue', alpha=0.5)
ax.scatter(X_test[y_test == 1], y_test[y_test == 1], label='Class 1 (Test)', marker='s', color='red', alpha=0.5)
ax.plot(x_, model.predict_proba(x_)[:, 1], label='Logistic Regression Model', color='green')
ax.axhline(0.5, color='gray', linestyle='--', label='Decision Boundary (0.5)')
ax.set_xlabel('X')
ax.set_ylabel('Probability')
ax.set_title(f'Logistic Regression Example with score={model.score(X, y):.2f}')
ax.legend()

y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]

在这里,绿色线条对应于输入特征的线性组合。它对应于给定样本的线性组合的逻辑值。通过调整线性系数,这条绿色线的形状和位置会移动,以更好地匹配训练样本。然后它被用来预测新样本的类别和概率。

为了更好地理解,我们来看看一个二维例子,这个例子更适合用于视觉线性分类。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.inspection import DecisionBoundaryDisplay

X, y = make_classification(n_samples=100, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LogisticRegression(C=10000000, max_iter=100000)
model.fit(X_train, y_train)

db = DecisionBoundaryDisplay.from_estimator(
    model,
    X,
    response_method="predict_proba", # "predict_proba",
    cmap="RdBu_r",
    alpha=0.5, grid_resolution=200,
)
sns.scatterplot(x=X_train[:, 0], y=X_train[:, 1], hue=y_train, palette={0:"blue", 1:"red"}, alpha=0.5, ax=db.ax_)
sns.scatterplot(x=X_test[:, 0],  y=X_test[:, 1], hue=y_test, palette={0:"blue", 1:"red"}, ax=db.ax_)

db.ax_.set_title(f"Decision boundary of the trained\n LogisticRegression with score={model.score(X, y):.2f}')

这个例子展示了模型如何通过一条一维线将二维输入特征划分开来。这条“线”代表逻辑函数,因此在阈值以上样本属于一个类别,而在另一侧则属于另一个类别。这里的重要思想是将前一个例子的推理扩展到更高维度。

像之前一样,这里是有关 sklearn 中的逻辑回归模型的附加信息:

  • LogisticRegression 接受更多的超参数,包括类似线性回归的fit_intercept,但也有其他参数允许调整正则化——我们将在下面进一步讨论这些。

  • 与线性回归一样,学习到的系数可以通过model.coef_model.intercept_访问。此外,你还可以通过model.classes_获取遇到的类列表。

  • 默认的评分是准确度,这只是正确分类的百分比:0 表示没有正确预测,1 表示所有预测都正确。

再次强调,关于分类的线性模型还有很多要说的,但这里的重点只是提供一个简单的例子。要了解更多关于 LogisticRegression 的内容,我强烈建议你去查看 sklearn 的用户指南。

处理非线性数据

到目前为止,我们已经看到线性回归和逻辑回归在合成数据上的例子,这些数据确实是线性的。换句话说,我们尝试用线性模型来逼近的真实情况确实是线性的。但在实际数据中,这几乎从未发生过,我们尝试建模和复制的系统通常是相当非线性的。

那么这是否意味着线性模型不够好?实际上并非如此,有解决方法。

除了使用天生非线性的模型(通过设计处理非线性数据的模型)外,我们还可以通过在输入数据中创建具有一定非线性的特征来使用线性模型。

换句话说,我们将使用相同的模型,但使用“更大”的输入数据矩阵,在其中我们“自己”添加包含输入特征之间非线性关系的新特征。

一个好的简单例子是上面介绍的多项式回归。假设我们想要拟合一个相对于单个特征 x 非线性的目标 y。对于标准线性模型,这个多项式回归只是 beta1 x + beta0 回归。如果我们创建新的特征,比如 x²和 x³,输入矩阵现在有 3 个特征,线性回归可以使用关系 y=beta3 x³ + beta2 x² + beta1x + beta0 来拟合目标。见下例:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline

X = 2 * np.random.rand(100, 1)
y = X**3 + 3 * X**2 + 0.5 * X + 2 + np.random.randn(100, 1)

linreg = LinearRegression()
linreg.fit(X, y)

degree = 3
poly_linreg = make_pipeline(PolynomialFeatures(degree), LinearRegression())
poly_linreg.fit(X, y)

x_ = np.linspace(0, 2, 100).reshape(-1, 1)

fig, ax = plt.subplots()
ax.scatter(X, y, label='Original Data')
ax.plot(x_, linreg.predict(x_), color='blue', label=f'Linear Regression (linreg) score={linreg.score(X, y):.2f}')
ax.plot(x_, poly_linreg.predict(x_), color='red', label=f'Polynomial Regression (poly_linreg, Degree {degree}) score={poly_linreg.score(X, y):.2f}')
ax.set_xlabel('X')
ax.set_ylabel('y')
ax.set_title('Fitting Linear and Polynomial Curves to Data')
ax.legend()

特别是,让我们检查线性回归和多项式回归的系数:

print(linreg.coef_, linreg.intercept_)
print(poly_linreg[-1].coef_, poly_linreg[-1].intercept_)
# [[10.607613]] [-2.32139028]
# [[-0.83958618  5.07382762  0.30530322]] [1.9408571]

对于简单的线性回归,我们只得到 beta1 值和 beta0 截距,但对于多项式回归,我们得到 3 个系数,分别对应 beta3、beta2 和 beta1,以及 beta0 截距。

使用 PolynomialFeatures 只是创建包含非线性的新特征的众多可能性之一。其他选项包括使用 KBinsDiscretizer(特别是encode='onehot')、SplineTransformer 或使用 Nystroem 的核方法,或者某些模型中实现的核技巧(例如,使用 SVR 进行回归和 SVC 进行分类的支持向量机模型)。

方法总是一样的:创建非线性的新特征并将其添加到输入数据中,以便线性模型可以利用这些特征来拟合复杂的 y 目标。好消息是,在 sklearn 中,所有这些方法都实现为管道中的预处理步骤,或内置于估计模型中。

正则化

到目前为止,我们已经看到如何使用基本的线性模型,既用于线性问题也用于通过添加新的非线性特征来处理非线性问题。

正则化的目的是改变或调整模型的学习方式,通常是通过改变目标/代价函数,以保持模型复杂度不过高。

从数学上讲,它通常是通过在问题的代价函数中添加一个项来实现的。例如,正则化的一个最简单的例子是线性回归,在这种情况下称为“岭回归”。经典的线性回归代价函数由平方误差的均值(或总和)给出:

使用正则化时,代价函数包含一个附加项,即向量 beta 的 L2 范数:

系数向量的范数由 alpha 超参数加权,以便我们可以修改其范数在最终解决方案中应有的影响程度。这样,在优化/学习过程中,beta 的系数不会变得过于庞大,而是会找到范数和误差之间的良好平衡。

这个概念可以应用于几乎任何其他模型,包括逻辑回归。

关于正则化,我们可以进一步讨论:就像我们看到的在管道/模型中调整超参数的重要性一样,岭回归的 alpha 参数也应该被优化(这适用于任何正则化参数)。

为此,我们可以使用 GridSearchCV 或 RandomSearchCV,正如在之前的模块中所见,但由于优化岭回归的 alpha 参数非常常见,sklearn 提供了一个 RidgeCV 模型,它接受一个 alpha 值列表进行测试,并使用交叉验证进行选择。

所以,让我们总结一下处理线性回归正则化的 4 种方法:

  1. 不使用正则化,使用 LinearRegression()

  2. 标准的未优化岭回归,使用 Ridge(),相当于 alpha=1

  3. 使用 GridSearch 或 RandomSearch 优化的岭回归

  4. 带有内置优化的岭回归,使用 RidgeCV

让我们通过视觉化的方式来看正则化如何影响线性回归的系数,因此使用岭回归模型。在以下示例中,我们使用 5 次多项式特征扩展进行线性回归,通过 Ridge 模型的正则化项及其 alpha 超参数来控制正则化的强度。

使用 alpha=0 时,没有正则化,我们得到经典的线性回归结果。由于我们使用高达 5 次特征来回归一个有噪声的线性关系,模型往往会有些过拟合,线性系数的范围很大(这里在 -1500 到 +1500 之间)。

使用 alpha=1 时,我们得到“轻微”正则化。模型过拟合的程度明显减少,系数的幅度也小了很多。

使用 alpha=100 时,我们得到非常强的正则化,因此系数不会增长很多,模型往往会出现欠拟合。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import Ridge
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline

X = 2 * np.random.rand(50, 1)
y = 4 + 3 * X + np.random.randn(50, 1)

# Function to fit and plot Ridge Regression models
def plot_ridge(alpha, ax1, ax2, deg=8):
    model = make_pipeline(PolynomialFeatures(deg, include_bias=False), Ridge(alpha=alpha))
    model.fit(X, y)

    ax1.scatter(X, y, color='blue', s=10, label='Data')

    x_range = np.linspace(0, 2, 100).reshape(-1, 1)
    y_pred = model.predict(x_range)
    ax1.plot(x_range, y_pred, color='red', label=f'Ridge Regression (alpha={alpha})')

    coefs = model.named_steps['ridge'].coef_.ravel()
    ax2.plot(range(deg), coefs, color='green', marker='o', label='Coefficients')

    ax1.set_title(f'Ridge Regression with alpha={alpha} / R²={model.score(X,y):.2f}')
    ax2.set_title(f"Linear coefficients with alpha={alpha}")
    ax1.legend()

fig, axs = plt.subplots(2, 3, figsize=(18, 6))
plot_ridge(0,   axs[0,0], axs[1,0])
plot_ridge(1,   axs[0,1], axs[1,1])
plot_ridge(100, axs[0,2], axs[1,2])
fig.tight_layout()

让我们进一步探讨 alpha 值如何影响训练分数和测试分数的变化:

from sklearn.model_selection import ValidationCurveDisplay
# Plotting the validation curve
ValidationCurveDisplay.from_estimator(
    make_pipeline(PolynomialFeatures(10, include_bias=False), Ridge()),
    X, y,
    param_name='ridge__alpha',
    param_range=np.logspace(-3, 3),
)

对于一个 10 次多项式,最佳正则化系数 alpha 似乎在 0.01 到 1 之间。

最后,请记住,就像创建新特征来处理非线性可以应用于几乎所有模型一样,正则化也可以包含在大多数模型中(包括带有其 C 参数的逻辑回归)。

总结

在这第 4 篇文章中,我们看到了:

  • 最重要的线性模型,即线性回归和逻辑回归

  • 如何通过创建新特征来处理非线性问题,例如创建多项式特征

  • 如何通过在目标函数中添加正则化项来控制模型的复杂度,以使线性系数不能任意增大

你可能会喜欢我的一些其他文章,确保查看一下:

Yoann Mocquin

Yoann Mocquin

时序的傅里叶变换

查看列表4 个故事Yoann Mocquin

Yoann Mocquin

科学/数值 Python

查看列表3 个故事具有讽刺意味的容器数组Yoann Mocquin

Yoann Mocquin

Sklearn 教程

查看列表9 个故事Yoann Mocquin

Yoann Mocquin

数据科学与机器学习

查看列表3 个故事!

使用“&”和“|”而不是“and”和“or”来切片 Pandas 数据框

原文:towardsdatascience.com/slicing-a-pandas-dataframe-using-and-instead-of-and-and-or-eca8fed7751

3 分钟 Pandas

当你看到 ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all() 时

YufengTowards Data Science Yufeng

·发表在 Towards Data Science ·阅读时间 6 分钟·2023 年 4 月 14 日

--

图片由 TJ Arnold 提供,来源于 Unsplash

如果你处理数据,数据过滤/切片是一项日常任务。

数据切片的一般思路是选择那些列值符合特定标准的行。例如,选择第二列值小于 3 的行、选择第三列值在预定义列表中的行、选择第五列值以‘ABC’开头的行等(详细了解如何切片)。

如果你在 Python 库pandas中进行数据切片,并使用andor运算符结合多个标准,你一定遇到过这种情况。

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all()

这是一个示例。让我们在 pandas 中创建一个数据框,

import pandas as pd
import numpy as np

data = {'name': ['John', 'Emily', 'David', 'Samantha', 'Michael', 'Amy', 'Mark', 'Jessica', 'Lucas', 'Maria'],
        'age': [25, 32, 19, 41, 28, 36, 24, 27, 30, 39],
        'city': ['New York', 'Paris', 'London', 'Tokyo', 'Los Angeles', 'Sydney', 'Toronto', 'Paris', 'Berlin', 'Rio de Janeiro'],
        'is_student': [True, False, True, False, False, True, True, False, False, True],
        'gpa': [3.7, 4.0, 3.2, 3.9, 3.5, 3.8, 3.1, 3.6, 3.3, 3.9],
        'income': [45000, 72000, 28000, 98000, 55000, 82000, 32000, 68000, 42000, 60000]}

# create the pandas DataFrame
df = pd.DataFrame(data)

数据框如下所示,

创建的玩具数据框(图片由作者提供)

然后,使用‘and’,我们尝试选择年龄不超过 25 岁但年收入至少为 50,000 的人。

df[(df['age']<=30) and (df['income']>=50000)]

这是我得到的结果,

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/13/qmh5_52s3m72pbywgbf08_ch0000gn/T/ipykernel_50721/396533384.py in <module>
----> 1 df[(df['age']<=30) and (df['income']>=50000)]

~/opt/anaconda3/lib/python3.9/site-packages/pandas/core/generic.py in __nonzero__(self)
   1525     @final
   1526     def __nonzero__(self):
-> 1527         raise ValueError(
   1528             f"The truth value of a {type(self).__name__} is ambiguous. "
   1529             "Use a.empty, a.bool(), a.item(), a.any() or a.all()."

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

我使用了and来将两个标准结合在一起,以便从数据框中获取满足两个标准的行。

我收到这个“值错误”,因为即使该 pandas series 由布尔值组成,将 pandas series 对象视为布尔对象仍然是模糊的。

具体来说,在表达式 (df[‘age’]<=30) and (df[‘income’]>=50000) 中,Python 尝试将布尔 pandas Series df[‘age’]<=30df[‘income’]>=50000 首先转换为单个布尔值。

这很模糊,因为 Python 不知道是应该仅当所有值都为True时才分配True,还是只要任何一个值为True。因此,结果出现了值错误。

解决此类问题非常简单,关键是避免将 pandas 系列转换为单一布尔值。

避免 pandas.Series()到布尔值的转换

在上面的例子中,我们实际上需要在两个 pandas 系列之间进行逐元素的and操作。来吧。

最简单的解决方案是将and改为&

df[(df['age']<=30) & (df['income']>=50000)]

结果为,

&而非and结果(作者提供的图片)

or操作也是一样的。

例如,我们希望选择那些仍然是学生或者年龄在 25 岁以下的人。

使用or将导致与使用and相同的值错误,

df[(df['age']<=25) or df['is_student']]
df[(df['age']<=25) or df['is_student']]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/13/qmh5_52s3m72pbywgbf08_ch0000gn/T/ipykernel_50721/1315814170.py in <module>
----> 1 df[(df['age']<=25) or df['is_student']]

~/opt/anaconda3/lib/python3.9/site-packages/pandas/core/generic.py in __nonzero__(self)
   1525     @final
   1526     def __nonzero__(self):
-> 1527         raise ValueError(
   1528             f"The truth value of a {type(self).__name__} is ambiguous. "
   1529             "Use a.empty, a.bool(), a.item(), a.any() or a.all()."

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

相反,我们应该这样做,

df[(df['age']<=25) | df['is_student']]

结果为,

|而非or结果(作者提供的图片)

当然,我们也可以使用 NumPy 函数来实现相同的操作,因为 pandas 系列是建立在 NumPy 数组之上的。

df[np.logical_and(df['age']<=30,df['income']>=50000)]

结果与使用&得到的相同,

np.logical_and()结果(作者提供的图片)

对于or操作也是类似的,

df[np.logical_or(df['age']<=25, df['is_student'])]

结果为,

np.logical_or()结果(作者提供的图片)

为了使代码看起来整洁,我建议在数据切片中使用&|

避免链式比较

有时你可能会感到困惑,即使使用了&|,仍然得到相同的错误。

你很可能做了链式比较。

例如,使用上述相同的数据,我们希望获取那些年龄在 20 到 25 岁之间且 GPA 至少为 3.0 的人。

df[(20 < df['age'] < 25) &
  (df['gpa'] >= 3)]

结果为,

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/13/qmh5_52s3m72pbywgbf08_ch0000gn/T/ipykernel_50721/1837177779.py in <module>
----> 1 df[(20 < df['age'] < 25) &
      2   (df['gpa'] >= 3)]

~/opt/anaconda3/lib/python3.9/site-packages/pandas/core/generic.py in __nonzero__(self)
   1525     @final
   1526     def __nonzero__(self):
-> 1527         raise ValueError(
   1528             f"The truth value of a {type(self).__name__} is ambiguous. "
   1529             "Use a.empty, a.bool(), a.item(), a.any() or a.all()."

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

不要将此错误与我们上面看到的错误混淆。虽然错误信息相同,但原因不再是&操作。这是关于20<df['age']<25的链式比较。

不幸的是,pandas 数据切片中不允许进行链式比较。

因此,链式比较必须拆分为两个独立的比较。

df[(20 < df['age']) &
   (df['age']< 25) &
  (df['gpa'] >= 3)]

现在我们得到了正确的结果,

避免链式比较(作者提供的图片)

并且不要忘记对每个你希望用&|结合的条件使用括号,否则操作符&|将比比较操作有更高的优先级。

例如,

df[20 < df['age'] &
   df['age']< 25 &
  df['gpa'] >= 3]

将给出相同的错误信息。

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/13/qmh5_52s3m72pbywgbf08_ch0000gn/T/ipykernel_50721/2058952526.py in <module>
----> 1 df[20 < df['age'] &
      2    df['age']< 25 &
      3   df['gpa'] >= 3]

~/opt/anaconda3/lib/python3.9/site-packages/pandas/core/generic.py in __nonzero__(self)
   1525     @final
   1526     def __nonzero__(self):
-> 1527         raise ValueError(
   1528             f"The truth value of a {type(self).__name__} is ambiguous. "
   1529             "Use a.empty, a.bool(), a.item(), a.any() or a.all()."

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

重点总结

  1. 使用&|而非andor来结合 pandas 数据切片中的比较。

  2. 避免使用链式比较作为数据切片的一个标准。

感谢阅读!希望你在工作中享受使用 Pandas 技巧!

如果你想阅读更多我的故事,请订阅我的 Medium。你还可以通过我的推荐链接加入 Medium 会员!

Pandas 中的滑动窗口

原文:towardsdatascience.com/sliding-windows-in-pandas-40b79edefa34?source=collection_archive---------1-----------------------#2023-03-29

使用重叠窗口技术识别时间序列数据中的模式

Avril AyshaTowards Data Science Avril Aysha

·

关注 发表于 Towards Data Science · 10 分钟阅读 · 2023 年 3 月 29 日

--

图片由 尤金·赫斯季亚科夫 提供,来自 Unsplash

窗口技术使数据分析师能够识别时间序列数据中的有价值模式。滑动窗口特别强大,因为它们允许你比其他技术更早地发现模式。在做出关键决策的几分钟(或几秒钟)早一点可能会为你节省资金,这是一项重要的功能。

本文将展示在pandas中执行窗口操作的三种方法,并讨论每种方法的权衡和好处。到最后,你将能够创建一个类似于下面这样的滑动窗口可视化:

作者提供的图片

想象一下你是 Uber 或 Lyft 的司机。你在纽约驾驶,等待下一个乘客……同时还在烧着昂贵的汽油。这不太好。你需要找到一种方法来最大化迅速获得新乘客的机会。你该怎么做?

你可以采取经验驱动的方法,经过一段时间测试不同策略来找出纽约的高需求区域。你可以尝试第一天从一个社区开始,第二天从另一个社区开始,依此类推……但纽约市很大,这样做需要花费很长时间才能开始以任何程度的信心识别出模式。

采用数据驱动的方法会更有效。这正是我们在本文中要做的。

开始吧 🚀

数据集

我们将使用来自纽约市出租车和豪华车委员会(TLC)旅行记录*数据集的 1 小时拼车(Uber/Lyft)数据,该数据集从 2022 年 1 月的完整数据集中子样本提取。你可以下载完整的 Parquet 文件(2022 年 1 月)。这包含了超过 1450 万次的乘车数据!()要复制本文使用的数据集,请将数据子样本提取到前 100K 行。你也可以直接访问数据和笔记本的专用 Github 仓库。

df = pd.read_parquet("fhvhv_tripdata_2022-01.parquet").head(100_000)

记住:我们的目标是识别需求峰值区域,定义为每单位时间的请求数量。

1. 快速粗略:对整个数据集进行 GroupBy

作者提供的图片

我们将开始对整个 1 小时数据集进行非常粗略的处理,以识别高需求区域。

在使用time_mask将数据子集提取到特定小时,并按request_datetime列排序值之后,这基本上就是一个 groupby 操作。

# define time mask
time_mask = (df['request_datetime'].dt.month == 1) & \
            (df['request_datetime'].dt.day == 1) & \
            (df['request_datetime'].dt.hour == 0) 

# sort values
one_hour = df[time_mask].sort_values(by=['request_datetime'])

# perform a simple groupby with .count()
high_demand = one_hour[['PULocationID','hvfhs_license_num']].groupby(['PULocationID']).count()

我们现在有每个取车地点的请求数量列表。让我们清理列名并排序值以获取有意义的输出。

high_demand = high_demand.rename(columns={'hvfhs_license_num':'n_requests'})
high_demand.sort_values(by='n_requests', ascending=False, inplace=True)
high_demand.head(10)

作者从[这个笔记本]生成的数据输出

太好了。我们现在可以识别出数据中需求最高的前 10 个取车区域。我们可以每小时批量运行一次这个代码。这很棒……但仅仅是这样。

以每小时为批次处理数据意味着司机只能在每小时结束时收到高需求区域的信号。如果高峰实际上发生在那个小时的前 15 分钟甚至 5 分钟内呢?那么,急匆匆地赶到那个城区只会发现高峰需求早已过去,毫无用处。

2. Pandas 中的滑动窗口

作者提供的图像

司机们不满意。我们需要做得更好。让我们编写一些代码,将我们的一小时数据分割成tumbling windows 每 15 分钟一个窗口。这样我们将获得更多的细节,并能给司机提供更有帮助的指示,告诉他们何时到达城市的哪个部分。

我们可以使用 pandas 的pivot_table().resample()方法来完成这个任务。我们需要将数据框的索引设置为一个 datetime 列。由于我们对请求数量感兴趣,我们将使用request_datetime列。

注意,数据集按请求的顺序未正确排序,并且包括一些在前一年午夜之前请求的行程。因此,让我们先清理这些数据。

# subset df to requests from 2022 only
requests = df[['request_datetime', 'on_scene_datetime', 'pickup_datetime', 'dropoff_datetime', 'PULocationID', 'DOLocationID']][df.request_datetime.dt.year==2022]

# sort by request_datetime
requests.sort_values(by=['request_datetime'], inplace=True)

# set request_datetime as index
requests.set_index('request_datetime', inplace=True)

requests.head()

我们的数据框现在看起来像这样:

作者生成的数据输出

记住,我们希望将数据集分组到 15 分钟的窗口中,每个窗口对应一个接送区的请求数量。这意味着我们希望输出具有 4 行(15 x 4 = 60 分钟)和每个 PULocationID 的列。

我们需要一个透视表:

# expand df to have a unique column for each PULocationID
pivot = pd.pivot_table(
    requests, 
    values='pickup_datetime', 
    index=requests.index,
    columns=['PULocationID'], 
    aggfunc='count'
)

pivot.head()

作者生成的数据输出

这是一个稀疏的数据框,对于每一行(请求),在对应于该请求的接送区的列中仅有一个计数(1)。

让我们使用.sum()对我们的数据框进行重采样,将数据分组到 15 分钟的窗口中,并对每个 PULocationID 的计数进行求和:

tumbling = pivo.resample('15min').sum()
tumbling

作者生成的数据输出

很好!我们现在有了每个 15 分钟窗口每个接送区的请求数量。剩下的就是准确地定义“高需求”的标准——假设为每 15 分钟窗口 40 个请求——然后我们可以使用这个数据框来筛选出超过高需求阈值的窗口和接送区。

我们还可以通过可视化数据来识别高峰需求区域:

res.resample('15min').sum().plot(legend=False)

作者生成的图表

这很好……但仍然只是有点好。我们仍然缺少很多细节。我们需要等待每个 15 分钟窗口完成后才能聚合数据。如果峰值发生在第 1 分钟到第 16 分钟之间的 15 分钟窗口(而不是第 0 分钟到第 15 分钟之间),我们将错过峰值一分钟,但需要等到 14 分钟后才能收到信号。到那时,可能已经太晚了,无法指示司机前往那个接送区。

不要害怕,完美就在眼前!

3. Pandas 中的滑动窗口

图片由作者提供

理想情况下,我们希望每分钟都有一个信号,告诉我们哪些区域正在经历高峰需求。我们还希望减少窗口的大小,以使我们的驱动程序能够更快地对 incoming 数据做出反应。因此,让我们使用 5 分钟的窗口,并将“高峰需求”定义为每 5 分钟窗口内超过 50 个请求

我们可以使用 pandas 的.rolling()方法来创建滑动窗口:5 分钟的窗口,重叠 4 分钟,以便我们可以每分钟跟踪 incoming 请求的速率。

你可以使用.resample().rolling()方法在 pandas 中创建滑动窗口。确保.resample()你所需信号间隔的大小,而不是窗口的大小:

# create sliding windows in pandas
res = pivot.resample(interval_size).sum()
windows = res.rolling(window_size).sum()

让我们一步步拆解。

我们希望输出是一系列行,每行是一个 5 分钟窗口,列中包含该 5 分钟窗口内每个取件区域的请求总数。然而,我们希望第二行不是从第一个 5 分钟窗口结束时开始,而是从第一个 5 分钟窗口的第一分钟结束时开始。

还跟得上吗?如果不行,也许下面的图示会有帮助:

图片由作者提供

我们真正想要的是 56 行每行 5 分钟,每行之间有 4 分钟的重叠。这将为我们提供每分钟的信号,告知哪些区域(如果有的话)每 5 分钟内请求超过 50 个。

为了实现这一点,我们需要首先重采样我们的 DataFrame,以便每分钟获取一行数据:

res_1m = res.resample('1min').sum()
res_1m.head(3)

作者生成的数据输出

(如果你想每秒钟获取一个信号,你可以在这里将采样频率调整为*1s*。)

现在每一行代表一分钟,我们可以使用rolling()sum()来获得每 5 行(分钟)的请求总数:

windows = res_1m.rolling(5).sum()
windows.head()

作者生成的数据输出

😱 哎呀,这么多 NaNs。

但这只是因为.rolling()默认将窗口操作(在此例中为总和)的输出放在窗口的右边缘。这在技术上是正确的,因为我们只能在第 4 分钟末获得第一个信号,当第一个窗口已完成时。

如果我们想去掉前 4 列,可以丢弃前 4 行空数据(分钟):

windows_drop = windows.iloc[4:]
windows_drop.head()

作者生成的数据输出

附注:前瞻性窗口

在某些情况下,你可能希望将值移动到左边缘,以使其更易读。如果你这样做,请小心不要犯错,误认为第 1 分钟时窗口 1 中实际上会有数据 (!) — 你实际上只是将结果移动到了第一行。

你可以使用FixedForwardWindowIndexer对象将窗口输出放在左边缘并创建前瞻性窗口:

# create custom window indexer
indexer = pd.api.indexers.FixedForwardWindowIndexer(window_size=5)

# use indexer to create rolling window
windows_f = res_1m.rolling(window=indexer).sum()

windows_f.head()

作者生成的数据输出

数据框的最后 4 列现在为空(因为在第 55 分钟之后没有完整的 5 分钟窗口),我们可以删除这些列。

# drop the last 14 columns
windows_f_drop = windows.iloc[:45]
windows_f_drop.tail()

作者生成的数据输出

使用 .rolling() 方法的这种方式并不广为人知,且在文档中稍微有些埋没,但一旦你理解了它的工作原理,使用起来还是相当简单的。

在 Pandas 中可视化滑动窗口

让我们可视化数据,以识别一些模式,并找到需求峰值最高的接送区域。我们将使用 windows 来明确显示前 5 分钟的数据缺失:

import matplotlib.pyplot as plt

windows.plot(kind='bar', legend=False);

作者生成的图表

这是一个非常粗略的可视化,但即便如此,我们也可以清晰地看到:

  1. 前 5 分钟的数据缺失

  2. 在特定区域内,接送请求的频率存在明显的峰值。

从这里,我们可以定义一个截断值/阈值,并识别在特定窗口中请求超过阈值的区域。然后我们可以鼓励司机去这些区域。

澄清图表的一种方法是删除所有值低于阈值的列。假设我们想要识别每 5 分钟窗口内请求超过 50 的区域:

signal = windows[(windows > 50)].dropna(axis=1, how=”all”)
signal.plot(figsize=(10,8))
plt.legend(bbox_to_anchor=(1.0,1.0))
plt.show()

作者生成的图表

这里还有很多可以做的事情。可以在 这个笔记本 中玩玩代码,进一步完善它。

Pandas 中的滑动窗口:结论

本文介绍了在时间序列数据上执行滑动窗口的强大功能。通过将数据集切割成小的、重叠的窗口,你现在可以以可操作的速率获得有价值的数据信号。

当然,理想情况下,你会实时完成所有这些操作。敬请关注有关实时处理的更多内容,但与此同时,你可能想查看一下 这个实时监控教程。做好准备,从纽约交通转到网站流量的背景变化 😉

希望你觉得这篇文章有帮助且有趣。 关注我的 LinkedIn 以获取我的最新内容。

数据处理愉快!👋

*纽约市出租车和豪华轿车委员会(TLC)行程记录数据于 2023 年 3 月 15 日访问,来源于 https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page。许可证: https://www.nyc.gov/home/terms-of-use.page

狡猾的科学:数据开采曝光

原文:towardsdatascience.com/sneaky-science-data-dredging-exposed-26a445f00e5c

从比萨饼到研究的黑暗面。图片由作者使用 Dall·E 3 创建。

探讨 p-hacking 的动机和后果

Hennie de HarderTowards Data Science Hennie de Harder

·发布在 Towards Data Science ·10 分钟阅读·2023 年 10 月 18 日

--

《纽约客》最近的头条新闻写道, “他们研究了不诚实。他们的工作是谎言吗?”。这背后有什么故事?行为经济学家丹·阿瑞里和行为科学家弗朗西斯卡·基诺,这两位在各自领域都很有声望的学者,正因涉嫌研究不端行为而受到审查。直言不讳地说,他们被指控捏造数据以获得统计上显著的结果。

不幸的是,这种情况并不罕见。科学研究中曾出现过一些欺诈行为。p-hacking 行为——例如操纵数据、在获得显著 p 值后停止实验,或仅报告显著结果——长期以来一直是一个关注的问题。在这篇文章中,我们将反思一些研究人员可能会被诱惑去篡改他们的发现的原因。我们将展示这些行为的后果,并解释你可以做些什么来防止在自己的实验中发生 p-hacking。

在我们深入探讨丑闻和秘密之前,先从基础开始——一个关于假设检验 101 的速成课程。这些知识将在我们探讨 p-hacking 的世界时非常有帮助。

假设检验 101

让我们回顾一下你需要了解的关键概念,以便完全理解这篇文章。如果你已经对假设检验(包括 p 值、I 型/II 型错误以及显著性水平)很熟悉,可以跳过这一部分。

最佳比萨测试

让我们前往那不勒斯,这座因比萨饼而闻名的意大利城市。两家比萨店,Port’Alba 和 Michele’s,自称制作世界上最好的比萨饼。你是一个好奇的美食评论家,决心找出哪家比萨店真正配得上这个称号。为了弄清楚,你决定举办“最佳比萨测试”(这本质上就是一个假设检验)。

你的调查始于两个假设

  • 零假设(H0):Port’Alba 和 Michele 的比萨在味道上没有差异;任何观察到的差异都是由于偶然。

  • 替代假设(H1):Port’Alba 和 Michele 的比萨在味道上存在显著差异,表明其中一个比另一个更好。

测试开始了。你召集一组参与者,组织一次盲品测试。每个参与者会被提供两片比萨,一片来自 Port’Alba,一片来自 Michele,而不知道哪一片是哪一片。参与者将对两片比萨进行评分(0-10)。

你设定了严格的alpha 水平(显著性水平)为 0.05。这意味着你愿意容忍 5%的 I 型错误概率,即在这种情况下,错误地声称一家比萨店的比萨更好而实际上并非如此。

在收集和分析数据后,你发现参与者 overwhelmingly 更喜欢 Michele 的比萨。以下是得分分布的样子:

图像由作者提供。

假设检验中有两个风险:

  • I 型错误:有 5%的小概率(即显著性水平 alpha),你可能会错误地得出 Michele 的比萨更好的结论,而实际上没有显著差异。你不想不公平地贬低 Port’Alba 比萨店。

  • II 型错误:另一方面,有 II 型错误。如果实际上 Michele 的比萨更好,但你的测试未能检测到这一点,你会觉得错过了最佳比萨!

在一个矩阵中(你可以将其与混淆矩阵进行比较):

I 型和 II 型错误的可视化。图像由作者提供。

为了确保你的发现,你计算p 值。结果是一个很小的数字,远小于 0.05。这意味着在零假设为真的情况下,得到如此极端结果的概率极低。我们有一个赢家!以下是用两样本 t 检验计算 p 值的示例:

import numpy as np
from scipy import stats

# Sample data for taste scores (out of 10) for the pizzerias
np.random.seed(42)
portalba_scores = np.random.normal(7.5, 1.5, 50)  
michele_scores = np.random.normal(8.5, 1.5, 50)
michele_scores = [round(min(score, 10), 1) for score in michele_scores]
portalba_scores = [round(min(score, 10), 1) for score in portalba_scores] 

# Perform a two-sample t-test
t_stat, p_value = stats.ttest_ind(portalba_scores, michele_scores)

# Set the significance level (alpha)
alpha = 0.05

# Compare the p-value to alpha to make a decision
if p_value < alpha:
    print("We reject the null hypothesis: {} < {}".format(round(p_value, 7), alpha))
    print("There is a significant difference in taste between Port'Alba's and Michele's pizzas.")
else:
    print("We fail to reject the null hypothesis: {} >= {}".format(round(p_value, 7), alpha))
    print("There is no significant difference in taste between Port'Alba's and Michele's pizzas.")

输出:

We reject the null hypothesis: 3.1e-06 < 0.05
There is a significant difference in taste between Port'Alba's and Michele's pizzas.

现在你对关键概念有了了解(希望在比萨故事后你不会饿),让我们继续本帖的隐秘部分:p-hacking。

图像由 Dall·E 3 生成,作者提供。

P-Hacking

在学术界,期刊充当知识的守门人。这些守门人偏好具有显著结果的研究。获得发表位置的压力可能非常大。这种偏见可能会 subtly 鼓励研究人员参与 p-hacking,以确保他们的工作能够公之于众。不幸的是,这种做法使科学文献中过度代表了积极结果。

P-hacking(也称为数据挖掘或数据窥探)定义为:

操作统计分析或实验设计以获得期望的结果或获得统计显著的 p 值。

因此,在研究中,你有可能操控你的分析和数据以获得有趣的结果以便发表。你不断调查、分析,有时甚至修改数据,直到 p 值显著。P-hacking 通常由对认可、发表的渴望以及显著发现的吸引力驱动。研究人员可能会无意或故意陷入 p-hacking 陷阱,被快速认可的承诺和竞争激烈的学术环境的压力所诱惑。

陷入这种陷阱的一个简单方法是对数据进行广泛探索。研究人员可能会发现自己处于数据集包含丰富变量和子组的情况下,等待探索。测试多种组合的诱惑可能很强。每一个变量、每一个子组都可能提供显著结果的机会。这里的陷阱是挑选数据——只突出那些支持预期结果的变量和子组,而忽略了所做的许多比较。这可能导致误导性和不具代表性的发现。

另一个陷阱是在实验正式结束前查看 p 值。这看起来可能无害,但事实并非如此。这里的陷阱是当达到理想的 p 值阈值时,提前停止数据收集的诱惑。这可能导致偏倚和不可靠的发现,因为样本可能不具代表性。即使有一个具代表性的样本,在结果给出显著 p 值的时刻停止数据收集也是对数据的操控。

数据探索和查看 p 值可能会不经意间发生。以下示例无法避免:在追求显著性时,研究人员可能会遇到初始结果未能达到预期的情况。调整主要结果的欲望,微妙地改变目标标准,可能会让人难以抗拒。

总结:如何避免 p-hacking?以下是五个提示和指导原则:

  • 避免广泛探索数据以寻找偶然的显著结果。如果你正在进行多重比较,请用Bonferroni 校正(将 alpha 值除以实验数量)来调整显著性水平。

  • 不要只报告那些产生显著 p 值的分析。保持透明,同时报告那些未产生显著结果的分析。这对于变量也是适用的:不要测试多个变量后仅报告那些显著的变量。在这种情况下,你是在挑选变量。更好的是,你可以考虑预注册你的研究,以提前声明你的假设和分析计划。

  • HARKing(在结果已知后假设):避免基于获得的结果形成假设;假设应在数据分析之前定义。

  • 避免在达到显著结果时停止数据收集或分析。与此相关的是:不要在测试结束之前查看结果。事先确定样本大小。是的,即使查看数据也是错误的!

  • 确保你满足统计测试的假设。例如,数据的正态性。如果你使用假设数据正态分布的测试,请确保数据确实是正态分布的(你可以通过Shapiro-Wilk测试来验证)。

由作者使用 Dall·E 3 创建的图像。

真实的(恐怖)故事

p-hacking 的后果是严重的,因为它导致假阳性(当零假设实际上为真时被拒绝)。研究是建立在先前研究基础上的,每一个假阳性都是对后续研究和决策的误导。此外,这还浪费了时间、金钱和资源,用于追求实际上不存在的研究方向。而也许最重要的后果是:它破坏了科学研究的信任和诚信。

现在,是时候谈论丑闻了…… 有许多数据操控、数据挖掘和 p-hacking 的例子。让我们看看一些真实的故事。

Diederik Stapel 的伪造研究

Diederik Stapel 是一位著名的荷兰社会心理学家,他在 2011 年被发现在多篇已发表的研究中伪造数据。他操控和伪造数据以支持他的假设,导致了心理学领域的重大丑闻。

在他被抓获之前,他发布了大量虚假的研究(根据撤回观察有 58 篇论文被撤回)。例如,Stapel 伪造了证明思考肉类会使人们变得更不社交的数据(这篇论文并未发表)。实际上发表的研究之一是关于杂乱环境如何促进歧视的。显然,当他的欺诈行为被揭穿时,这项研究也被撤回了。不过,你仍然可以在网上找到他的一些撤回论文

Boldt 案例

比之前的案例更令人震惊的是关于 Joachim Boldt 的案件。Boldt 曾被认为是医用胶体领域的杰出人物,并且是使用胶体羟乙基淀粉(HES)在手术过程中提高血压的坚定支持者。然而,一项故意排除 Boldt 的不可靠数据的荟萃分析揭示了不同的故事。这项分析显示,与其他复苏液体相比,静脉注射羟乙基淀粉与显著更高的死亡风险和急性肾损伤相关。《电讯报》上的一则标题

数百万患者曾因世界顶级麻醉学家之一的“欺诈性研究”而接受争议药物治疗。

正如你所预期的,这些揭露的后果非常严重。Boldt 失去了他的教授职位,并成为刑事调查的对象,面临伪造多达 90 项研究的指控。

是什么让他发布这些虚假的结果?为什么有人愿意冒生命危险来追求某种出版物或宣传?不幸的是,在 Boldt 的案例中,我们不得而知。虽然他可能在调查或法律程序中提供了解释或陈述,但没有对他的动机进行全面且广泛认可的描述。这里可以找到一篇关于该案件的文章,其中包含时间线。

再现性危机

网络上还有许多更多的恐怖故事。一般来说,研究的再现性非常重要。为什么?它允许验证和确认研究结果,确保结果不是仅仅由于偶然、错误或偏见。但是再现结果可能是具有挑战性的。实验条件可能会有所不同,设备差异,或者数据收集和分析中的微小错误。(或者有人进行了 p-hacking,这使得无法重现研究。)

一些研究人员试图展示再现性为何成为问题。一个有趣的例子是一项关于巧克力以及其对体重减轻影响的研究。尽管方法学上存在故意的缺陷,但该研究仍被广泛发布和分享。这一事件展示了薄弱的科学严谨性和耸人听闻的媒体报道的陷阱。

但在尝试重现现有研究时,也会遇到挑战。2011 年,拜耳的研究人员透露,他们只能重复约四分之一 (!) 的已发布的临床前研究。这一令人担忧的发现使许多人质疑一些早期药物研究的可靠性,强调了良好验证流程的迫切需求。同样,心理学领域也面临自己的问题。为了解决这个问题,“许多实验室”项目 被启动了。这个全球性努力让世界各地的实验室尝试重现相同的心理学研究,常常揭示出结果的显著差异。这些发现强调了多实验室协作确保研究结果真实的关键作用。

不幸的是,这并非事实。图片由作者使用 Dall·E 3 创建。

结论

这篇文章旨在让你意识到 p-hacking 对科学文献的微妙但重大影响。这提醒我们,并非每篇研究文章都可以轻信。作为好奇且批判性思考者,接近科学发现时必须保持敏锐的眼光。不要盲目相信每一篇出版物;相反,寻求多个支持相同论断的来源。当不同研究趋于相同的真理时,真正的知识才会显现。

但如果有一个解决学术界最紧迫挑战之一的方案呢?请在我即将发布的博客文章中关注我揭示 p-hacking 问题的潜在解药。敬请期待!

相关内容

[## 拥抱未知:数据科学家从混沌理论中获得的教训

理解预测模型局限性的见解

medium.com ## 机器学习项目中的伦理考虑

在构建 AI 系统时不要忽视这些话题

[towardsdatascience.com

所以你的 AI 究竟有多公平?

原文:towardsdatascience.com/so-how-fair-is-your-ai-exactly-83f8defcf449?source=collection_archive---------14-----------------------#2023-02-24

关于准确实现公平目标的挑战

Boris RufTowards Data Science Boris Ruf

·

关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 2 月 24 日

--

图片由 Pawel Czerwinski 提供,来源于 Unsplash

人工智能(AI)的使用引发了新的伦理和法律挑战。在 我之前的文章 中,我说明了为什么从训练数据中移除敏感信息并不能促进公平,反而相反。本文讨论了为 AI 应用选择最合适的公平性定义。这个专门的工具最初是在一篇 研究论文 中提出的,我在这方面有共同发表。

通过 AI 收集和分析数据已成为标准做法,机器学习如今在许多领域改善了商业表现。然而,近年来暴露了许多 AI 机器偏见的案例,并且新的例子不断出现。这是 AI 的主要陷阱之一:如果训练数据包含任何形式的偏见,算法将会纳入并强化这些偏见——这可能会对性别、宗教、民族或年龄等敏感子群体造成伤害。

定义“公平性”的挑战

当前的偏见缓解方法主要是技术性的,专注于调整算法或数据以满足某种公平性。然而,存在多种相互冲突的公平性概念,遗憾的是没有普遍接受的定义。最合适的公平性度量始终取决于应用的背景。在实践中,确定公平性目标是复杂的,因为将伦理原则映射到度量标准不是一个简单的过程。不过,为了获得更公平 AI 的可持续解决方案,必须将注意力集中在这个问题上,因为当实施的公平性目标与利益相关者的期望不一致时,最佳的技术缓解方法将会失败。

分类任务的可用公平性度量。图片由作者提供。

不在同一页面上

在许多情况下,这一决定并非轻而易举,正如围绕 COMPAS 算法的激烈辩论所显现的那样。该算法由 Northpointe 公司开发,用于生成一个独立的数据派生的“风险评分”,以评估多种形式的再犯风险。这种算法在美国刑事司法部门中被用来支持法官做出特定决定,如批准保释或假释。评分具有信息性质,最终决定仍由法官做出。

2016 年 5 月,调查性新闻网站 ProPublica 将注意力集中在 COMPAS 算法可能存在的种族偏见上。其主要论点基于对数据的分析,显示结果存在偏差。特别是,对于黑人群体的假阳性率显著高于白人群体。因此,黑人被不成比例地错误地归因于高复发风险。另一方面,Northpointe 对这些指控的回应是,算法实际上在两个群体之间实现了预测值平等。简而言之,这确保了风险评分与重新犯罪的概率相符,无论肤色如何。

从客观的角度来看,可以说双方对数据做出了有效且合理的观察。然而,巨大的争议揭示了精确定义和披露应用程序所选公平目标是绝对关键的。而这个决定通常涉及仲裁和妥协。在给定的场景中,例如,只有在满足以下条件之一时,两个公平目标才能互相满足:要么敏感子群体的基本比率完全相同,要么结果类别完全可分,从而能够创建一个实现完美准确性的理想分类器。不幸的是,这两个要求在现实世界中都非常不可能满足。

导航指标

令人惊讶的是,关于如何简化实际应用中的公平选择过程的研究相对较少。为了解决这个挑战,我们开发了Fairness Compass,这是一个实验工具,旨在结构化复杂的公平指标领域。基于一系列关于数据性质、对其正确性的信念、公平政策以及模型应关注的特异性或灵敏度的问题,Fairness Compass 引导 AI 从业者找到最适合给定场景的选项。将这一选择过程形式化并转化为简单的程序,有助于消除在现实世界中实施负责任的 AI 的障碍。此外,记录决策背后的理由可以作为内部文档,并作为沟通手段来增加透明度,促进对技术的信任。

Fairness Compass 已作为开源项目发布在GitHub上。它曾获得Gartner Eye on Innovation award提名,并被世界经济论坛纳入AI Fairness Global Library

示例应用程序

为了说明这个概念,让我们以人力资源领域的一个示例场景为例。作为敏感子群体,我们考虑男性和女性。需要回答的问题是,在评估员工晋升决策的公平性时,哪种公平定义最为合适。请注意,这只是一个虚构的思维实验,根据具体情况,其他答案可能会有不同的结果。Fairness Compass 的目的是基于给定场景的定义要求,支持做出明智的决策。

B. Ruf 和 M. Detyniecki,“AI 中的公平性之正确方向”,ECML/PKDD 2021(工业追踪)

在上面的动画中,公平性指针被表示为一个包含三种不同类型节点的决策树:钻石形状表示决策点;白色框表示行动,灰色圆角框表示公平性定义。连接节点的箭头代表可能的选择。

让我们开始这个过程。第一个问题是关于可能影响决策的现有政策。公平性目标可以超越对不同群体或类似个体的平等待遇。如果目标是通过提升弱势群体来弥合现存的不平等,积极行动或配额可以是有效的措施。这种目标可能源自法律、法规或内部组织指南。这种方法排除了敏感属性与结果之间的任何可能因果关系。如果数据在不同子群体之间的基准率有所不同,这是一个强烈的承诺,这意味着将算法的准确性服从于政策的总体目标。例如,许多大学通过接收更多来自弱势背景的学生来提高多样性。这些招生政策承认来自敏感子群体的学生具有同等高的学术潜力,并将他们可能较低的教育水平视为社会中的不公正,而不是个人缺陷。

对于我们的示例场景,我们得出结论,促销决策没有实行这种积极行动政策。因此,我们选择“否”并记录我们选择的理由。现在,我们继续下一个问题,并重复该过程,直到到达一个叶节点,该节点包含为定义的用例推荐的公平性定义。

因此,遵循这样的形式化过程可以显著有助于识别和解释特定 AI 应用的最佳公平性度量。

所以呢

在实践中,存在许多不同的公平性定义。由于一些定义是互相排斥的,因此必须选择其中之一。做出选择并非易事,因为最佳决策始终取决于应用的背景,权衡往往是不可避免的。因此,选择 AI 应用的公平性目标时需要特别小心,并且还需将其传达给内部和外部利益相关者。关于这一决策背后的透明性是实现更公平 AI 的关键因素。

非常感谢 Antoine Pietri 在撰写这篇文章中的宝贵支持。在 以下文章中,我将概述如何主动减少 AI 应用中的偏见。

参考文献

N. Mehrabi, F. Morstatter 等 (2021). 机器学习中的偏见与公平性调查。ACM 计算调查 (CSUR) 54, 6 (2021), 1–35。

J. Angwin, J. Larson 等 (2016). 机器偏见。数据与分析伦理,Auerbach 出版社,254–264。

W. Dieterich, C. Mendoza 等 (2016). COMPAS 风险量表:展示准确性、公平性和预测一致性。Northpointe Inc.

S. Corbett-Davies & S. Goel (2018). 公平性的测量与误测量:对公平机器学习的批判性回顾arXiv:1808.00023

P. Saleiro, B. Kuester 等 (2018). Aequitas:一个偏见与公平性审计工具包arXiv:1811.05577

K. Makhlouf, S. Zhioua 等 (2021). 关于机器学习公平性概念的适用性。ACM SIGKDD 探索新闻通讯。23, 1, 14–23。

B. Ruf & M. Detyniecki (2021). 迈向正确的人工智能公平性。ECML/PKDD 2021 行业专场。

Sobel 算子在图像处理中的应用

原文:towardsdatascience.com/sobel-operator-in-image-processing-1d7cdda8cadb

Sobel 算子是什么及其用法示例

Egor HowellTowards Data Science Egor Howell

·发布于 Towards Data Science ·7 分钟阅读·2023 年 12 月 31 日

--

href=”www.flaticon.com/free-icons/image-processing" title=”图像处理图标” 图像处理图标由 juicy_fish 创建 — Flaticon。

卷积概述

在我之前的文章中,我们深入探讨了 卷积神经网络 (CNNs) 的关键构建块,即卷积数学算子。我强烈建议你查看那篇文章,因为它为这篇文章提供了背景和理解:

## 卷积解释 — 卷积神经网络简介

CNNs 的基本构建块

towardsdatascience.com

简而言之,图像处理中的卷积是我们将一个小矩阵,称为核,应用于输入图像,以创建一个应用了某种效果的输出图像,如模糊或锐化。

从数学上讲,我们得到的是:

离散卷积定理。作者在 LaTeX 中的方程。

  • fg:** 函数 fg 之间的卷积。

  • f: 输入图像

  • g: 核矩阵,也称为滤波器

  • t: 卷积正在计算的像素位置。

  • f(τ): 图像 f 在像素 τ 处的像素值。

  • g(t−τ): gt 处被 τ 移动后的像素值。

以下是该过程的示例,我们对输入图像应用盒状模糊

这是一个对灰度图像应用模糊效果的卷积示例。图示由作者创建。

图像模糊示例。图示由作者在 Python 中创建。

卷积通过将输入图像的每个像素与卷积核中的相应元素相乘并求和这些乘积,然后按元素数量进行归一化来计算。

这是如上图所示的中间像素的示例:

[30*1 + 30*1 + 30*1] +
[30*1 + 70*1 + 30*1] +
[30*1 + 30*1 + 30*1] 

= 30 + 30 + 30 + 30 + 70 + 30 + 30 + 30 + 30 = 310

pixel value = 310 / 9 ~ 34

根据卷积核的结构,我们可以对图像应用不同的效果。最有用的卷积核之一是Sobel 算子,这也是本文的主题!

什么是 Sobel 算子?

Sobel 算子是一个强大的边缘检测卷积核,它对机器学习特别是卷积神经网络非常有用。它通过计算图像在每个像素处的强度梯度(一级导数)的近似值来实现这一点。

Sobel 算子由两个卷积核组成,一个测量水平方向上的梯度,x,另一个测量垂直方向上的梯度,y。在数学上,它的形式如下:

Sobel 算子。公式由作者在 LaTeX 中提供。

这些卷积核与输入图像进行卷积,以计算我们所考虑的像素两侧的强度差异。

由于它们的大小为3x3,它们计算速度也很快,这对于处理深度学习和 CNNs 是一个巨大的好处。然而,这意味着它的分辨率较小,因此可能会检测到人眼认为不“真实”的边缘。

图像通常没有完美的水平或垂直边缘,因此我们需要计算梯度的总幅度。可以按照如下步骤进行计算:

梯度幅度。公式由作者在 LaTeX 中提供。

还有一个没有幂的版本,它计算速度更快但只是一个近似值:

梯度幅度的近似值。公式由作者在 LaTeX 中提供。

我们还可以通过使用arctan来计算边缘的角度:

边缘的角度。公式由作者在 LaTeX 中提供。

需要注意的是,Sobel 算子主要用于灰度图像,因为它测量强度的变化。

Sobel 算子示例

现在,让我们通过一个示例来演示如何将 Sobel 算子应用于图像。

以下是我们的灰度输入图像,大小为5x5 像素:

Image:

  0   0   0   0   0
  0   0   0   0   0
200 200 200 200 200
200 200 200 200 200
200 200 200 200 200

如我们所见,前两行全是黑色,后三行全是白色,表明存在一个垂直边缘。

像素值越小,颜色越暗。值为 0 表示黑色,值为 255 表示白色,中间的值表示灰度级。

这些是 Sobel 算子:

G_x:

-1  0  +1
-2  0  +2
-1  0  +1

G_y:

-1  -2  -1
 0   0   0
+1  +2  +1

现在让我们将 Sobel 算子应用于输入图像的中央3x3像素网格:

Central Grid:

  0   0   0
255 255 255
255 255 255

Apply G_x:

(0*-1 + 0*0 + 0*1) +
(255*-2 + 255*0 + 255*2) +
(255*-1 + 255*0 + 255*1) =

0 + 0 + 0 +
-510 + 0 + 510 +
-255 + 0 + 255 = 0

Apply G_y:

(0*-1 + 0*-2 + 0*-1) +
(255*0 + 255*0 + 255*0) +
(255*1 + 255*2 + 255*1) =

0 + 0 + 0 +
0 + 0 + 0 +
255 + 510 + 255 = 1020

G_x为零,意味着在水平方向上没有梯度,这在我们查看输入图像的像素值时是有道理的。

梯度的幅值为:

G = (G_x² + G_y²)⁰.5 = (0² + 1020²)⁰.5 = 1020

以及角度:

theta = arctan(G_y/G_x) = arctan(1020/0) = undefined

由于除以零是未定义的,我们可以推断方向完全是垂直的,这意味着我们有一个水平边缘。

这些都可以在 Python 中完成,如下所示:

# Import packages
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage

# Input image
image = np.array([
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
    [200, 200, 200, 200, 200],
    [200, 200, 200, 200, 200],
    [200, 200, 200, 200, 200]
])

# Sobel Operators
G_x = np.array([
    [-1, 0, +1],
    [-2, 0, +2],
    [-1, 0, +1]
])

G_y = np.array([
    [-1, -2, -1],
    [0, 0, 0],
    [+1, +2, +1]
])

# Apply the Sobel kernels to the image
output_x = ndimage.convolve(image.astype(float), G_x)
output_y = ndimage.convolve(image.astype(float), G_y)

# Define the light grey color for the background
light_grey = [0.8, 0.8, 0.8]  # RGB values for light grey

# Normalize the Sobel outputs
norm_output_x = (output_x - output_x.min()) / (output_x.max() - output_x.min())
norm_output_y = (output_y - output_y.min()) / (output_y.max() - output_y.min())

# Set the background color of the plots
plt.rcParams['figure.facecolor'] = light_grey
plt.rcParams['axes.facecolor'] = light_grey

# Plot the images with the light grey background
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

axes[0].imshow(image, cmap='gray')
axes[0].set_title('Original Image')
axes[0].axis('off')

axes[1].imshow(norm_output_x, cmap='gray')
axes[1].set_title('Sobel - Horizontal Direction')
axes[1].axis('off')

axes[2].imshow(norm_output_y, cmap='gray')
axes[2].set_title('Sobel - Vertical Direction')
axes[2].axis('off')

plt.tight_layout()
plt.show()

Sobel 算子示例。由作者在 Python 中生成的图表。

水平 Sobel 算子未定义/为零,因为没有水平边缘。而垂直 Sobel 算子有强烈的响应,检测到黑色矩形显示的边缘区域。

代码可在我的 GitHub 上找到:

## Medium-Articles/Neural Networks/sobel_operator.py at main · egorhowell/Medium-Articles

这是我在 Medium 博客/文章中使用的代码。通过创建一个帐户来参与 egorhowell/Medium-Articles 的开发…

github.com

其他边缘检测器

Sobel 并不是唯一的边缘检测核/算法。以下是一些其他常见的算法的列表,供感兴趣的读者参考:

  • Canny 边缘检测器: 可能是最受欢迎的边缘检测器,但有多个阶段,包括高斯模糊、梯度检测和抑制。

  • Prewitt 算子: 非常类似于 Sobel 算子,因为它也使用两个 3x3 的卷积核来测量两个方向上的梯度强度。然而,Prewitt 算子的准确度略低于 Sobel,但对噪声的敏感度较低。

  • Robert Cross 算子: 另一种差分算子,但使用 2x2 的卷积核,因此计算时间更快。

总结与进一步思考

在本文中,我们展示了如何使用 Sobel 算子检测图像中的边缘。该算子计算图像中单个像素的离散近似梯度。如果像素两侧的差异很大,它很可能位于边缘上。这是卷积神经网络通过找到这些类型的卷积核来工作的核心。

另外一件事!

我有一个免费的新闻通讯,数据深度解析,我在其中分享成为更优秀的数据科学家的每周技巧。没有“花里胡哨”的内容或“点击诱饵”,只有来自实际数据科学家的纯粹可操作的见解。

## 数据深度解析 | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读由 Egor Howell 发布的 Substack 文章《Dishing The Data》…

newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----1d7cdda8cadb--------------------------------)

与我联系!

参考资料和进一步阅读

使用 NetworkX 进行社会网络分析:温和的介绍

原文:towardsdatascience.com/social-network-analysis-with-networkx-a-gentle-introduction-6123eddced3

了解像 Facebook 和 LinkedIn 这样的公司如何从网络中提取洞察

Riccardo AndreoniTowards Data Science Riccardo Andreoni

·发布于Towards Data Science ·8 分钟阅读·2023 年 9 月 28 日

--

图片来源:pixabay.com

像 Netflix 这样的流媒体平台是如何推荐完美的电影给你度过晚上?我们如何优化运动中的球员位置?我们如何识别金融网络中的欺诈活动?我们如何预测约会应用中的匹配

所有这些挑战都有相同的解决方案:社会网络分析

社会网络分析(SNA)是一个强大的数据科学领域,它可以探索网络中实体之间复杂的关系,发现隐藏的模式并捕捉洞察。网络实体可以是各种事物:它们可以是社交网络上的人,比如 Instagram 上的用户,也可以是互联网上的网站,甚至是比赛网络中的运动队。

在接下来的部分中,我将帮助你理解数据科学中的网络是什么,我将描述最常见的类型,并实现几个算法以从网络中提取有价值的信息。

所有理论部分将结合实际示例。你会找到几个Python 代码片段,以帮助你熟悉NetworkX Python 库。

理解网络

网络,也称为图,是信息的特定表示,包括两个基本组成部分:节点

节点是网络的构建块。它们代表网络中的个体实体。它们可以是社交网络中的人,也可以是生物网络中的基因。

则表示节点之间的关系。它们可以定义两个人之间的互动、关联或依赖关系。边可以是有向的,这意味着它们表示的关系从一个节点到另一个节点。想象一个有向边表示一个人向另一个人支付的钱,了解资金流向的方向确实是有益的。这种不对称用箭头表示。

NetworkX 中,节点和边的表示方式如下:

import networkx as nx

# Create an instance of a graph
G = nx.Graph()

# Create an edge between two nodes. The nodes will be also created.
G.add_edge('A', 'B', weight=6)

上面的“Hello world”代码的结果是这个小而简单的网络:

图片由作者提供。

然而,类似地,NetworkX 库可以轻松创建更复杂的网络,如下所示:

图片由作者提供。数据源

网络类型

在建模现实世界网络时,我们必须考虑到存在多种结构。我将简要介绍最常见的网络,并展示如何使用 NetworkX 模型化它们。

有向和无向网络

有向网络具有定义的方向。这表示两个节点之间关系的不对称。例如,在电子邮件网络中,边的方向可以指示哪个节点发送电子邮件,哪个节点接收。

另一方面,无向网络忽略任何方向,意味着节点之间的关系是对称的。一个例子可能是友谊网络,通常关系是相互的,或体育赛事网络。

在 NetworkX 中,用于分别实例化有向和无向网络的代码是 DiGraph()Graph()。我使用 Numpy 库随机生成合成网络:

# Synthetic Directed Graph
Gd = nx.DiGraph()
for i in range(30):
    node_1 = np.random.randint(10)
    node_2 = np.random.randint(10)
    if node_1 != node_2:
        Gd.add_edge(node_1, node_2)   

# Synthetic Undirected Graph
G = nx.Graph()
for i in range(30):
    node_1 = np.random.randint(10)
    node_2 = np.random.randint(10)
    if node_1 != node_2:
        G.add_edge(node_1, node_2) 

你可以通过一些美丽的互动可视化来探索图形:

合成有向图。图片由作者提供。

合成无向图。图片由作者提供。

加权网络

在某些情况下,实体之间的关系是不平等的。我们可以通过给每条边加上权重来衡量关系的“强度”。权重反映了两个节点之间交互的重要性。

以航班网络为例。每个机场代表一个节点,边代表它们之间的航班。为每条边分配权重,以建模如航班时长、距离或票价等信息是很方便的。

要在 NetworkX 中创建一个简单的加权网络,我们需要初始化一个空的 nx.Graph() 对象。第二步是使用 add_edge() 方法添加边,并包含权重属性。

# Initialize the graph
G = nx.Graph()

# Add weighted edges
G.add_edge("A", "B", weight=0.6)
G.add_edge("A", "C", weight=0.2)
G.add_edge("C", "D", weight=0.1)
G.add_edge("C", "E", weight=0.7)
G.add_edge("C", "F", weight=0.9)
G.add_edge("A", "D", weight=0.3)

最后,我们可以可视化加权网络。

合成加权图。图片由作者提供。

一种加权网络的变体是有符号网络。它们通过分配符号(+ 或 -)来考虑每个关系的正负性。

一个有向网络的例子是在线产品评价网络。这样的网络将产品和客户表示为节点,客户评价为边。边可以有正号(+)或负号(-),分别表示正面评价或负面评价。

在 NetworkX 中,有符号图的创建和加权图一样简单:

# Initialize the graph
G = nx.Graph()

# Add weighted edges
G.add_edge("A", "B", sign='+')
G.add_edge("A", "C", sign='-')
G.add_edge("C", "D", sign='-')
G.add_edge("C", "E", sign='+')
G.add_edge("C", "F", sign='-')
G.add_edge("A", "D", sign='+')

从网络中提取信息

一旦我们理解了网络可以呈现的不同形式,我们需要知道如何提取有意义的洞察。我们可能希望找到关键节点或边,并发现隐藏的模式。幸运的是,已经开发了多种算法来回答这些问题。

PageRank 算法

PageRank 是一种著名的算法,用于衡量网络中节点的重要性。它最初由拉里·佩奇(Larry Page)和谢尔盖·布林(Sergey Brin)在谷歌开发,并在搜索引擎网页排名中取得了卓越的成果。

PageRank 算法的核心思想是,重要的节点是那些从其他网络节点,特别是从其他重要节点接收许多入边的节点。

PageRank 算法遵循的步骤很简单:

  1. 为每个节点分配初始 PageRank 分数,等于 1/N(其中 N 是网络中的总节点数)。

  2. 应用基本的 PageRank 更新规则。该规则指出,每个节点将其 PageRank 分数的等分额提供给其邻居节点。

  3. 每个节点的新 PageRank 分数更新为其从邻居节点接收到的值之和。

这些步骤会重复进行,直到 PageRank 分数收敛到稳定值。

如果我们以以下 25 个节点的网络为例,这个网络是我合成生成的,极其复杂,很难确定哪个是最有影响力的。

使用 PAM 算法生成的合成网络。图片由作者提供。

我们可能有一些线索,但 PageRank 算法通过为每个节点的重要性分配一个数学值,使我们的生活更容易。幸运的是,NetworkX 具有内置的 PageRank 方法。

# Generate an artificial network of 25 nodes
G = nx.barabasi_albert_graph(25,4, seed=42)

# Apply the PageRank algorithm and store the scores in a pd dataframe
pagerank_results = nx.pagerank(G, alpha=0.85, max_iter=100, tol=1e-06)
pagerank_results = pd.Series(pagerank_results).sort_values(ascending=False)

# Plot the most importan node scores
fig, ax = plt.subplots()
sns.barplot(x=pagerank_results.iloc[:10].values, y=pagerank_results.iloc[:10].index.astype(str), orient='h', alpha=0.75)
ax.set_xlabel('PageRank Score')
ax.set_ylabel('Node')
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
for i in ax.containers:
    ax.bar_label(i,fmt='%.2f')

从结果中我们可以清楚地看到,节点 0、7 和 6 拥有最高的 PageRank 分数。

图片由作者提供。

HITS 算法

HITS 算法旨在识别网络中的两种重要节点类型:中心节点和权威节点。

  • 中心节点是指指向许多其他节点的节点。

  • 权威是指许多中心节点所指向的节点。

与 PageRank 不同,HITS 不仅可以衡量网络中每个节点的重要性,还可以推断出其在图中的角色。

HITS 算法遵循的步骤是:

  1. 为网络中的每个节点分配一个权威分数和一个中心分数,初始值均为 1。

  2. 对每个节点应用 Authority 更新规则。一个节点的 Authority 分数是指向它的节点的 Hub 分数之和。

  3. 对每个节点应用 Hub 更新规则。一个节点的 Hub 分数是它指向的节点的 Authority 分数之和。

每次迭代后,我们都会更新 Authority 和 Hub 分数。经过大量迭代后,这两个分数应该会收敛。

感谢 NetworkX 库,我们可以将 HITS 结果与 NetworkX 提供的结果进行比较。

# Convert the previously create graph to directed
G_dir = G.to_directed()
for a, b in list(G.edges()):
    G_dir.remove_edge(a, b)

# Compute HITS scores
hits_results = nx.hits(G_dir, max_iter=100, tol=1e-06)
authority_results = pd.Series(hits_results[1]).sort_values(ascending=False)
hubs_results = pd.Series(hits_results[0]).sort_values(ascending=False)

# Plot the results
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2)
fig.set_figheight(8)
fig.set_figwidth(16)
sns.barplot(x=authority_results.iloc[:10].values, y=authority_results.iloc[:10].index.astype(str), orient='h', alpha=0.75, ax=ax1)
ax1.set_xlabel('Authority Score')
ax1.set_ylabel('Node')
ax1.spines['top'].set_visible(False)
ax1.spines['bottom'].set_visible(False)
ax1.spines['right'].set_visible(False)
ax1.spines['left'].set_visible(False)
for i in ax1.containers:
    ax1.bar_label(i,fmt='%.2f')

sns.barplot(x=hubs_results.iloc[:10].values, y=hubs_results.iloc[:10].index.astype(str), orient='h', alpha=0.75, ax=ax2)
ax2.set_xlabel('Hub Score')
ax2.set_ylabel('Node')
ax2.spines['top'].set_visible(False)
ax2.spines['bottom'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
for i in ax2.containers:
    ax2.bar_label(i,fmt='%.2f')

我们可以清楚地看到,具有较高 Authority 分数的节点也往往具有较高的 PageRank 分数。节点 0 在这两个度量中都排名第一,节点 7、6 和 5 排名也靠前。

图片由作者提供。

PageRank 缺少的是节点 13、8、10 和 20 的“Hub”角色。它们与相关节点连接,因此我们可以断言它们在网络中发挥了重要作用。

结论

在这篇文章中,我们看到如何通过像 NetworkX 这样的专业库使从网络中提取信息变得更加容易。大多数度量标准和网络算法可以通过一个函数来计算。

我们所看到的只是机器学习中的社交网络分析的简介,这些主题应该成为每个数据科学家工具包的一部分。虽然我们触及了一些基本概念,但该领域的内容远超这些入门见解。

我推荐深入研究本文附带的资源和参考资料。

如果你喜欢这个故事,可以考虑关注我,以便获取我即将发布的项目和文章的通知!

这是我过去的一些项目:

## 使用 Scikit-Learn 进行集成学习:友好的入门指南

像 XGBoost 或随机森林这样的集成学习算法是 Kaggle 竞赛中的顶级模型之一……

towardsdatascience.com ## 使用深度学习生成幻想名称:从零开始构建语言模型

语言模型能否创造独特的幻想角色名称?让我们从头开始构建它

towardsdatascience.com

参考文献

在数据分析中,软技能胜过技术技能

原文:towardsdatascience.com/soft-skills-beat-technical-skills-in-data-analytics-57f56ca91361?source=collection_archive---------0-----------------------#2023-06-08

这些软技能帮助了我的职业发展

Matthew GazzanoTowards Data Science Matthew Gazzano

·

关注 发表于 Towards Data Science ·6 分钟阅读·2023 年 6 月 8 日

--

照片HeadwayUnsplash

有一个常见的误解,即进入这一领域的人认为写出更好的代码会让你成为更好的数据分析师。我在这里告诉你,这根本不是事实。

数据分析师最重要的功能是以可衡量的方式为组织创造商业价值。这并不意味着你需要一个令人印象深刻的技术解决方案。

你的角色是解释一个业务问题,为利益相关者构建一个叙述,解释事件背后的原因,并展示其对业务的影响。这可能需要使用先进工具的技术方法,但这并不是最终重要的。

数据分析并不是:

我们本月失去了 14 个客户。”

更像是:

“与上个月相比,客户流失率增加了 5%,其中 75%的流失客户提到他们转向了竞争对手。这些客户特别指出我们的产品缺少某个功能,这解释了下降的原因。”

后者给利益相关者提供了可以行动的内容,正如你生成的所有报告应该做的那样。

考虑以下软技能将有助于提升你在这一角色中的有效性,并使你脱颖而出。

对商业模型的深入理解

换句话说,组织是如何产生收入的?你对交付产品或服务的复杂性有多熟悉?你的领域知识应专注于你报告的业务领域(如财务、运营、营销等),以及对整个组织的整体了解。你对业务本身了解得越多,你在生成分析和产生影响时的方法就会更好。

这也意味着你了解源系统如何收集和验证你报告的数据。你应当了解构成某些指标的操作流程的细节,以及在此基础上能够回答哪些问题的限制。如果你是一个电子商务公司——如何从 Google Analytics 中测量页面浏览量?或者如果你在供应链工作,“完成时间”代表什么以及它在 SAP 中是如何展示的?

很多优秀的数据分析师来自特定领域的协调员/分析师角色(如财务分析师、营销协调员等)。这是一条有效的路径,因为在业务过程的基层能让你对组织的运作有更多的了解。

想一想,如果你在一家航空公司工作,开发一个航班预订的仪表板,难道在预订部门工作的经历不会给你带来不公平的优势吗?你将了解其预订系统中所有相关的数据点,它们对业务的意义以及围绕这一过程的挑战。

照片Dylan GillisUnsplash 提供

沟通与协作

不可避免地,作为数据分析师,你不会对所面临的业务问题拥有所有答案。因此,你需要与其他团队合作,以澄清你所需的特定数据或生成某项内容的业务流程。

数据分析师的角色也是面向组织的,你可能需要向同事、管理层、C-Suite 或外部客户展示你的发现。你需要重新整理你的演示技巧,能够以有意义的方式沟通复杂的结果。

在沟通时,简洁总是更好。你的同事、利益相关者和管理层不想听你如何操作你的 Pandas Dataframe 或在你的 SQL 语句中有多少 公共表表达式。相反,你需要能够抽象出你工作的技术方面,仅沟通最重要的内容。

批判性思维、好奇心和可观察性

你将面临的问题是困难的。如果不是这样,你的组织就不会雇佣你。

当面对一个业务问题时,你需要构思可用的数据是什么,以及如何将其转化为正确的答案。将初始问题陈述与可用数据连接起来可能并不总是很清楚,但持续对“为什么”的好奇心将始终让你在这个领域取得成功。

例如,假设你的组织要求你创建一个报告,说明每月有多少客户合同被升级销售。从表面上看,这个问题似乎很简单,但当你到达可用数据时,它变得更加复杂。你需要首先确定合同条款是如何识别的,以便按月汇总。你只是参考合同记录上的结束日期,还是需要查看账单系统以验证是否仍在收款?也许是两者的混合。

一旦你知道合同条款是如何识别的,如何确定它是升级销售、降级销售、平价续订还是取消呢?我们是否需要查看合同期前后的账户净变化?还是需要参考其他标准?

在这种情况下,你需要能够将大问题分解成小问题,并在探索性数据分析(EDA)阶段进行自我组织。

卓越的利益相关者管理

利益相关者是数据分析师的客户;他们是从你生成的报告中获益并采取行动的人。就像其他任何事情一样,如果客户对你的服务不满意,他们会寻求其他地方的帮助。

数据分析师需要了解他们报告的受众的角色。考虑一下你的利益相关者可能关注的周边问题以及他们在工作职能中个人关心的内容。在运营和财务领域,我常常需要回答的问题是“为什么 X 数字与 Y 数字不一致?”。能够向我的利益相关者解释这种原因,总是有助于对话向积极方向发展。

成功与利益相关者合作也涉及到人际因素。问问自己以下问题:

  • 你是否易于相处?

  • 你是否让他们更容易与您沟通需求?

  • 他们理解你分析结果的难度如何?

  • 当你说你可以在项目截止日期前达成基准时,你能做到吗?如果不能(这没关系),你能否透明地说明阻碍因素?

尽管你需要确保回答所有被要求的内容,利益相关者管理也意味着在正确的情境下给予适当的反驳。但这其中有一条细微的界限。你可能听说过项目管理中的术语“范围蔓延”。在获得项目反馈时,这确实是一个有效的关注点。在与利益相关者进行仪表盘或报告审查时,确保得到必需的改进与可选改进的清单作为要点。

询问“你为什么需要 X 指标?”是可以的,因为这可能会引发有意义的对话。但如果你发现尽管你努力质疑这个新功能,对话仍然没有进展,可能对你来说,放下争议,优先完成任务会更有益。

最后的思考

是的,你仍然需要技术技能,但没有人关心这些技能。

在你的角色中,技术技能是成功的必要条件。数据永远不会以你可以简单地SELECT * FROM perfect_table的格式出现。但是要知道,除了你自己之外,可能没有其他人会看到报告背后的技术解决方案。

当然,你仍然需要考虑仪表盘或报告的可扩展性,以确保其在你的分析团队中易于维护且具有动态性。但不要对不需要复杂的东西过度设计。

在我的一个角色中,我们最受关注的一个 Tableau 仪表盘是由一位销售经理开发的。它看起来不够美观,也没有任何先进的功能。但这个仪表盘以惊人的简洁性和重要性解决了一个关键的销售目标。成功的关键在于这位经理对销售团队主要关切的深刻理解。

所以,是的,仍然去学习 SQL、Python、Excel 和 Tableau。你会需要它们。但请确保你理解为什么需要它们。

软技能是你在数据科学面试中脱颖而出的关键

原文:towardsdatascience.com/soft-skills-is-what-sets-you-apart-in-your-data-science-interviews-d927872f07e6

图片由 Jason Goodman 提供,来源于 Unsplash

如何提升你的结构化问题解决技能和沟通技能

Tessa XieTowards Data Science Tessa Xie

·发表于 Towards Data Science ·5 分钟阅读·2023 年 12 月 24 日

--

所以你已经复习了机器学习概念,练习了几个月的 Python 和 SQL,觉得面试准备差不多了。但你可能忽略了面试中最重要且最难准备的部分——解决问题的能力。根据我面试他人以及自己接受面试的经验,我可以自信地告诉你,这部分常常决定了你的面试成败。

这是数据科学面试指南的第五篇文章,在之前的文章中,我主要涉及了在面试中常见的技术概念。供参考,之前的文章如下:

  1. 第一部分:分布

  2. 第二部分:概率

  3. 第三部分:基本监督学习模型

  4. 第四部分:随机森林

但与大多数人不同的是,我认为技术部分的面试不是最难的部分;我认为在面试准备中,软技能是最难掌握、学习或教授的。作为一名经理,这也是我在招聘团队成员时最看重的因素。因为说实话,技术技能是容易复习和学习的(不仅对人类如此,对机器也一样——ChatGPT 现在可以很好地实现代码),但理解和解决业务问题以及有效沟通的能力不是你一两天内就能培养起来的。

这部分面试也是最不可预测的,因为面试官没有固定的模板;相反,很多时候面试会按照你引导的方向进行。在你感到气馁之前,我总结了一些软技能在面试中如何被测试的方法和技巧,可能会帮助你为这部分面试做准备。

一般来说,面试有两个模块属于软技能类别——案例研究(用于测试你进行结构化问题解决的能力)和行为面试(用于测试你的沟通能力以及结构化思维能力)。

将业务问题转化为数据科学问题的能力——案例研究

这部分面试通常是一个非常类似于咨询案例面试的案例,但在此基础上增加了一些数据科学的味道。它通常是一个关于指标变化或业务决策的根本原因分析(RCA)类型的问题。例如,像“我们日活跃用户在过去一周减少了 10%,如何调试找出原因?”或者“我们想在旧金山周边为我们的车队设置电动充电器,我们应该如何决定放置位置?”

这在测试什么? 这些问题是在测试你提出解决问题框架的能力以及解释该框架的能力。作为数据科学家,在日常工作中拥有一个这样的框架至关重要,以确保你的解决方案全面、彻底,并且能够让他人轻松理解。

面试官想看到什么? 请记住,对于这类问题没有对错之分,只有组织好的或组织不好的回答。面试官真正想看到的是你如何组织你的思路,并引导他人了解问题解决的过程。所以确保你明确表达你所做的所有假设、你探索的所有细节,并提出你可能需要的所有澄清问题。

还很重要的一点是展示你接受反馈和与他人合作的能力。有时你的面试官会给你一些指导或暗示,告诉你他们希望你如何推进案例。

如何准备? 准备案例面试的最佳方法就是练习、练习再练习。我建议你找一本或两本关于咨询案例面试或 PM 案例面试的书,例如破解 PM 面试案例面试秘密,这些都是推荐的书籍。但仅仅通过阅读书中的例子并强迫记忆框架是不够的。在我看来,最有效的方法是从书中选择样例案例,并与朋友进行模拟。你的回答很可能不会与“样本解决方案”完全相同,但请让你的朋友检查“我的思考过程是否易于跟随?”、“每一步的过程是否清晰?”

文化契合度——沟通技能和工作风格

测试什么? 面试官希望看到你除了硬技能之外,是否具备团队所寻求的其他品质。如果面试官是招聘经理,他们会希望看到你是否能够有效沟通和合作。

面试官想要看到什么? 根据团队的不同,面试官/招聘经理可能会关心/想要测试不同的方面。但在大多数情况下,我看到的面试问题都旨在考察你的学习能力、适应变化的能力、与他人协作的能力和管理利益相关者的能力。

如何准备? 有几个方面可以帮助你在面试中表现更好。1)确保你对简历上的任何项目非常熟悉,因为面试官很可能会询问简历上不同项目的细节,以了解你的工作风格。2)确保你练习自上而下的沟通风格,并在谈论你的项目时遵循STAR 框架

最后,请记住,面试是双向评估的过程;你在评估公司是否适合你,就如同他们在评估你一样,而面试中的行为部分是测试你是否喜欢他们的最佳方式。面试你的人员可能是你将要密切合作的一组人;所以如果你感觉到“氛围不对”(相信我,这种情况比你预期的要多),这可能是你在决定是否接受 offer 时需要考虑的一个良好信号。

准备数据科学面试,并想阅读更多关于面试准备的文章吗?以下是一些你可能会喜欢的文章!

## 精通麦肯锡数据科学面试中的机器学习部分

详细指南:咨询面试中机器学习部分的“什么、为什么和如何”

[towardsdatascience.com ## 数据科学家的生产力技巧

如何作为数据科学家更好、更智能地工作,但不一定更辛苦

[towardsdatascience.com

编写可维护机器学习代码的软件工程最佳实践

原文:towardsdatascience.com/software-engineering-best-practices-for-writing-maintainable-ml-code-717934bd5590

一位迷失在代码森林中的数据科学家。与第二个和最后一个建议相关。图片由作者通过Midjourney创建。

针对数据科学家的高级编码建议

Hennie de HarderTowards Data Science Hennie de Harder

·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 8 月 6 日

--

与传统的软件工程项目不同,机器学习代码库由于其复杂和不断发展的特性,往往在代码质量上滞后,导致技术债务增加和协作困难。优先考虑可维护性对于创建稳健的机器学习解决方案至关重要,这些解决方案能够适应、扩展并随着时间的推移提供价值。

近年来,机器学习在全球掀起了热潮,从医疗到金融等各个行业都发生了变革。随着越来越多的组织加入机器学习的行列,探索新的可能性和洞察力,编写可维护和稳健的机器学习代码变得至关重要。通过编写易于处理且经得起时间考验的机器学习代码,团队可以更好地协作,并在模型和项目成长和适应的过程中确保成功。以下部分将展示机器学习代码库中的常见示例,并解释如何妥善处理这些问题。

不要创建大型单体结构

这个建议可能与你无关,但它是为那些尚未意识到这一点的个体(直到现在)编写的!

单体脚本,即一个脚本处理整个项目,可能会在你将实验代码复用到生产环境时出现。复制、粘贴、完成!创建一个单独的脚本来处理整个项目总是一个糟糕的主意。这种脚本难以阅读(即使是编写者也难以阅读)、难以调试且效率低下。你无法轻松添加新功能或修改代码,因为每次都需要运行整个脚本。添加单元测试也是不可能的,因为这个单体就是“整个单元”。

单个脚本的另一个问题是可重用性。你不能在其他项目中重用这些代码,因为它很难阅读。

写单体应用只有一个理由;那就是如果你不喜欢接手你工作的同事。如果你想让这个人感到沮丧,这是一个简单的方法。

那么应该怎么做呢?编写模块和类。创建具有单一特定用途的不同代码文件。每个文件应包含函数或类和方法。这样,代码会更容易阅读、调试、重用和测试。在下一个提示中,你可以找到一个常用的目录结构。

不要过度设计仓库结构

这可能看起来有些违反直觉,但这是非常重要的。过度设计仓库结构指的是为代码和项目文件创建复杂且不必要的复杂组织。这涉及引入抽象层、过度的文件夹结构和复杂的命名约定,这些可能在可维护性、可扩展性或协作方面没有显著好处。相反,它增加了不必要的复杂性,使团队成员更难理解、导航和贡献项目。

可能出现的一些问题包括:由于开发者的认知负担增加,开发速度变慢。新团队成员的学习曲线变得更加复杂,而仓库的复杂性可能导致代码重复和碎片化。

如何维持一个健康的仓库结构?以下是一些可能有帮助的提示:

  • 遵循在机器学习社区中广泛接受的标准目录布局和命名约定(见下文)。

  • 将相关文件和模块分组放在适当的目录中。例如,将数据预处理代码与模型训练代码分开。

  • 提供清晰简洁的文档,以指导团队成员如何浏览和贡献项目。

  • 定期与团队审查仓库结构,以识别可以简化或改进的区域。

  • 最后但同样重要的是:保持简单!避免添加不必要的抽象层或复杂的文件夹层级,除非它们确实能改善组织和可读性。

这里是一个标准目录结构的例子:

project_root/
|-- data/
|   |-- raw/                   # Raw data files (read-only)
|   |-- processed/             # Processed data files (generated by scripts)
|   |-- intermediate/          # Intermediate data files (generated by scripts)
|-- notebooks/                # Jupyter notebooks (for exploration, analysis, and visualization)
|-- src/
|   |-- data/                  # Data processing scripts
|   |-- models/                # Model implementation and training scripts
|   |-- evaluation/            # Model evaluation and testing scripts
|   |-- utils/                 # Utility functions and helper scripts
|-- experiments/              # Experiment logs, metrics, and model checkpoints
|-- configs/                  # Configuration files (hyperparameters, settings)
|-- tests/                    # Unit tests and test data
|-- docs/                     # Documentation files (if separate from notebooks)
|-- README.md                 # Project overview and instructions
|-- pyproject.toml            # Poetry project file for package management
|-- poetry.lock               # Poetry lock file for pinned dependencies
|-- Dockerfile                # Dockerfile for containerization
|-- .gitignore                # List of files to ignore in version control (e.g., data files, virtual environments)

许多嵌套的文件夹可能使仓库过于复杂。照片由Didssph拍摄,来源于Unsplash

了解你的编程语言行为

在某些情况下,编程语言的行为可能与预期不同。这可能会导致挫折和大量的调试时间。为了防止这种情况,如果你了解编程语言的奇怪行为,会大有帮助。

如果我们看看 Python,以下是一些你应该注意的例子。

作为程序员,你可能会遇到的第一个例子是以下情况。你想要对数字进行四舍五入,然后你发现了这个问题:

print(round(2.5))  # output: 2 (expectation: 3) (!)
print(round(3.5))  # output: 4 (expectation: 4)
print(round(4.5))  # output: 4 (expectation: 5) (!)
pirnt(round(5.5))  # output: 6 (expectation: 6)

发生了什么?在 Python 3 中,决定使用银行家舍入,这意味着 0.5 会四舍五入为偶数。为什么这样做有意义?我喜欢这个解释在 Stack Overflow 上。

另一个 Python 中奇怪行为的例子:

def append_to_list(item, my_list=[]):
    my_list.append(item)
    return my_list

print(append_to_list(1))  # output: [1]
print(append_to_list(2))  # output: [1, 2] (!)

在上面的例子中,列表的默认参数是一个空列表。但是如果你第二次运行该函数,第二次运行的输出会令人惊讶。它返回的是之前的列表加上新的项!原因是,在 Python 中存在可变默认参数,这意味着对默认对象的更改会在函数调用之间持续存在。为避免这种情况,将默认值设置为None,并在函数调用中创建一个新的可变对象实例。

另一个例子是浮点运算精度

print(0.1 + 0.2 == 0.3)  # output: False (!)

在比较浮点值时,最好接受小的容差。如果你在测试期间比较浮点值,并且使用了pytest,你可以使用approx解决这个问题:

from pytest import approx

def test_example():
    assert 0.1 + 0.2 == approx(0.3)  # output: True

如果你使用的是 Python 2.7,以下示例可能会让人困惑。列表推导式中使用的变量可能会“泄漏”到外部作用域:

x = 5
my_list = [x for x in range(3)]
print(x)  # output: 2 (!)

None进行排序会引发TypeError

my_list = [None, 3, 1, 2]
sorted_list = sorted(my_list)  # raises TypeError (!)

你还可以将属性分配给一个类中不存在的实例:

class MyClass:
    pass

obj = MyClass()
obj.new_attribute = 42
print(obj.new_attribute)  # output: 42 (!)

另一个令人困惑的例子是不一致的闭包。闭包是一个函数,它捕获并记住它创建时的环境,包括来自外部作用域的任何变量。虽然闭包是一个强大的特性,但它们在循环中的行为可能会导致令人惊讶的结果:

def create_multipliers():
    multipliers = []
    for i in range(5):
        def multiplier(x):
            return i * x
        multipliers.append(multiplier)
    return multipliers

multipliers_list = create_multipliers()

# calling the closure functions from the list
print(multipliers_list0)  # output: 8 (4 * 2)
print(multipliers_list1)  # output: 8 (4 * 2)
print(multipliers_list2)  # output: 8 (4 * 2)
print(multipliers_list3)  # output: 8 (4 * 2)
print(multipliers_list4)  # output: 8 (4 * 2)

所有函数都返回循环中i的最后一个值(即 4)与x的乘积。这是因为 Python 中的闭包闭合的是变量,而不是它们的值,这意味着它们保留对变量i的引用。当闭包稍后被调用(在循环外部时),它会查找其封闭作用域中i的当前值,现在是 4(循环中的最后一个值)。

解决这个问题的一个简单方法是使用默认参数:

def create_multipliers_fixed():
    multipliers = []
    for i in range(5):
        def multiplier(x, i=i):
            return i * x
        multipliers.append(multiplier)
    return multipliers

multipliers_list_fixed = create_multipliers_fixed()

# calling the fixed closure functions from the list
print(multipliers_list_fixed0)  # output: 0 (0 * 2)
print(multipliers_list_fixed1)  # output: 2 (1 * 2)
print(multipliers_list_fixed2)  # output: 4 (2 * 2)
print(multipliers_list_fixed3)  # output: 6 (3 * 2)
print(multipliers_list_fixed4)  # output: 8 (4 * 2)

除了这些示例,还有许多其他例子。意识到这一点将使你的代码更加健壮和可靠,并避免所有那些意外的惊喜!

处理多个返回值

在函数的返回语句中添加越来越多的参数可能会使 Python 代码变得混乱且更难维护,特别是当参数数量增加时。每次你更改某些内容时,你需要更新所有调用代码,这会导致维护上的噩梦。开发人员可能会错误地按错误的顺序传递参数,从而导致逻辑错误。

Python 提供了一种优雅的解决方案:Python 的collections模块中的[namedtuple](https://docs.python.org/3/library/collections.html#collections.namedtuple)

以下是如何使用 namedtuples 来提高代码的清晰度和可维护性:

from collections import namedtuple

def calculate_statistics(numbers):
    total = sum(numbers)
    mean = total / len(numbers)
    maximum = max(numbers)
    Statistics = namedtuple('Statistics', ['sum', 'mean', 'maximum'])
    return Statistics(sum=total, mean=mean, maximum=maximum)

# example
data = [12, 5, 8, 14, 10]
result = calculate_statistics(data)

print("Sum:", result.sum)         # output: Sum: 49
print("Mean:", result.mean)       # output: Mean: 9.8
print("Maximum:", result.maximum) # output: Maximum: 14

简单,对吧?使用namedtuple有许多好处,比如提高代码的可读性,保持不变性,并且更节省内存(用 C 实现)。对于你作为程序员来说,最大的好处也许是:在向返回语句添加另一个参数后,你不需要更新所有调用代码。

注意:namedtuples在 Python 中类似于 Scala 中的case classes

异常处理注意事项

处理异常的最糟糕方法是发生异常时盲目继续:

try:
    result = do_something()
except:
    pass

永远不要这样做。如果发生了某些事情且未能获得结果,脚本会像一切正常一样继续。异常被默默忽略。这可能导致隐藏的错误和意外的行为。

在一些代码库中,你会发现许多try except块。避免使用 try-except 块进行正常流程控制。异常应该用于处理异常情况,而不是常规流程。

另一种不良实践是捕获泛型异常:

try:
    result = do_something()
except Exception as e:
    log_error(f'Exception occurred: {str(e)}')

这可能会隐藏特定错误,使调试变得困难。还要避免裸except块。你将对发生了什么一无所知。

你应该如何处理异常?以下是一些有用的提示。

尽量具体,只捕获泛型异常用于调试。在某些情况下,添加finally块可能会很有用。finally块中的代码将始终运行。

try:
    result = do_something()
except FileNotFoundError:
    log_error("File not found.")
except ValueError:
    log_error("Invalid input.")
except Exception as e:
    # catch any other unexpected exceptions and log them for debugging
    log_error(f"An unexpected error occurred: {str(e)}")
finally:
    # this runs always, whether an exception occurred or not
    close_resources()

你还可以创建自定义异常类。通过自定义异常,你可以提供更具体的错误消息,并帮助区分不同类型的错误。可以像这样简单:

class CustomError(Exception):
    def __init__(self, message, *args):
        self.message = message
        super(CustomError, self).__init__(message, *args)

def some_function():
    if some_condition:
        raise CustomError("This is a custom error message.")

try:
    some_function()
except CustomError as ce:
    log_error(str(ce))

适当地处理大型条件逻辑树

(业务)逻辑的复杂性可以迅速升级。它从一个简单的 if-else 语句开始,但随着扩展,变成一个庞大的条件结构,变得难以管理,导致挫败感。幸运的是,存在多种方法来解决这一挑战并提高可维护性。

一种解决方案是将逻辑与处理分开,例如在字典中。让我们来看一下下面的 if-else 树:

def process_input(x):
    if x == 'A':
        return 'Apple'
    elif x == 'B':
        return 'Banana'
    elif x == 'C':
        return 'Cherry'
    elif x == 'D':
        return 'Date'
    # ... and so on for many more cases
    else:
        return 'Unknown'

我们可以使用字典对其进行重构:

def process_input(x):
    mapping = {
        'A': 'Apple',
        'B': 'Banana',
        'C': 'Cherry',
        'D': 'Date',
        # ... and so on for many more cases
    }
    return mapping.get(x, 'Unknown')

最好在函数外部定义映射,在单独的设置或配置文件中。这种方法是可以的,但如果不小心,它仍然可能变成一个相当大的字典。

处理这种情况的另一种方法是使用多态。创建一个带有通用接口的基类,然后为每个特定情况实现子类。每个子类将处理其独特的逻辑。

假设我们想根据输入op执行不同的数学操作,并将它们应用于xy

class Operation:
    def perform(self, x, y):
        raise NotImplementedError

class Addition(Operation):
    def perform(self, x, y):
        return x + y

class Subtraction(Operation):
    def perform(self, x, y):
        return x - y

class Multiplication(Operation):
    def perform(self, x, y):
        return x * y

class Division(Operation):
    def perform(self, x, y):
        if y != 0:
            return x / y
        else:
            raise ValueError("Cannot divide by zero.")

def calculate(op, x, y):
    operations = {
        '+': Addition(),
        '-': Subtraction(),
        '*': Multiplication(),
        '/': Division(),
    }

    operation = operations.get(op)
    if operation:
        return operation.perform(x, y)
    else:
        raise ValueError("Invalid operation.")

最后一个警告:要小心代码重复。如果你有多个模型并且希望使用它们的输出得到最终得分,不要创建嵌套的条件逻辑树。相反,你可以使用多个小函数来得到最终得分。

让我们来看一个学校里的玩具示例。你的数学最终成绩将根据你的出勤率和考试百分比分数来计算。

首先看看这个:

def final_score(attendance: float, exam: float) -> int:
    if attendance < 0.25:
        if exam < 0.25:
            return 4
        elif exam < 0.5:
            return 5
        elif exam < 0.75:
            return 6
        else:
            return 7
    elif attendance < 0.5:
        if exam < 0.25:
            return 5
        elif exam < 0.5:
            return 6
        elif exam < 0.75:
            return 7
        else:
            return 8
    elif attendance < 0.75:
        if exam < 0.25:
            return 6
        elif exam < 0.5:
            return 7
        elif exam < 0.75:
            return 8
        else:
            return 9
    else:
        if exam < 0.25:
            return 7
        elif exam < 0.5:
            return 8
        elif exam < 0.75:
            return 9
        else:
            return 10 

看起来复杂对吧?如果计算成绩时涉及更多因素怎么办?每增加一个参数,树形结构将呈指数增长。

幸运的是,对于这种情况,存在一种更简洁高效的解决方案:

from typing import List

def map_score(score, score_ranges: List[float]) -> int:
    for i, threshold in enumerate(score_ranges):
        if score < threshold:
            return 2*i
    return 2*(i+1)

def final_score(parameter_scores: List[float], base_score: int = 4) -> int:
    parameter_range = [0.25, 0.5, 0.75]
    scores = [map_score(parameter_score, parameter_range)/len(parameter_scores) for parameter_score in parameter_scores]
    return sum(scores) + base_score

# example
attendance = 0.6
exam = 0.85
result = final_score([attendance, exam])
print("Final Score:", result)

这个解决方案扩展性很好!你可以将一个参数及其对应的分数添加到列表中,它将被包括在结果中。

注意:你可能希望在添加更多参数时对最终解决方案进行四舍五入。确保正确执行(参见有关编程语言行为的提示)。

未来你可以用这些示例作为灵感,编写更智能的代码,避免冗长的条件逻辑树。

结论

恭喜你读完了这篇文章!提供的建议对于保持代码库结构化和可维护性非常有帮助。在混乱的单体文件和过于复杂的嵌套目录之间取得平衡是获得良好开发体验的关键。

通过理解编程语言的行为、采用命名元组处理多个返回值、有效处理错误和简化条件逻辑,开发者和团队可以减少维护时间,更多地投入到添加有用功能或探索新项目中。记得用测试代码检查代码格式化文档来加强代码库,以确保长期健康和生产力。

编程愉快,下次再见!

相关

## 简化你的机器学习项目

为什么花费大量时间和精力在复杂模型上是一个不好的主意,以及应该怎么做

## 如何简化你的机器学习项目 ## 如何有效比较机器学习解决方案

增加将模型投入生产的机会

## 如何有效比较机器学习解决方案 ## 使用机器学习检测数据漂移

通过一个简单的自动化过程,了解你的机器学习模型性能下降的情况。

medium.com

敏捷项目中的软件规范

原文:towardsdatascience.com/software-specification-in-agile-projects-8248f5be6c1

IBM 最大转型项目的原始见解

Thomas ReineckeTowards Data Science Thomas Reinecke

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 10 日

--

传统的瀑布式和 V 模型软件开发依赖于大量前期时间投资来指定解决方案、集成或功能,通常导致广泛的文档。这些方法因其不灵活、高成本和无法适应实时变化而不适用于敏捷过程。

然而,人们常常认为敏捷团队在没有规范的情况下工作,因为他们优先考虑接受变化和交付产品,而不是文档。其实并非如此。即使是敏捷项目,不论其规模如何,仍然需要一定程度的结构指导治理文档来确保一致性和防止混乱。这对于涉及多个开发小组和数百名工程师的大规模项目尤为重要。

本文提供了对我们在 IBM 实施的一些最大内部转型项目的模式和方法的未经过滤的见解。

图片来源:ThisisEngineering RAEngUnsplash

为什么结构、指导、治理?

假设你正在创建一个涉及多个小组的复杂产品,那么敏捷 Scrum 团队自给自足且任何需求都可以分配给任何团队的假设不可避免地会引出以下问题:

  • 谁负责拥有数据模型,或至少是其特定部分或对象?

  • 我们如何在图书馆、实用类和应重新使用的服务等基础能力上达成一致?

  • 对于哪些数据元素,产品成为记录系统(SoR),而哪些只是消费者或处理器?

  • 我们如何对上下游数据流进行对齐和标准化?

  • 谁负责领导和标准化数字体验,包括与设计系统、组件库、外壳、页面布局和设计原则相关的问题?

  • 我们如何向用户通知即将到来的或已部署的变更?

  • 测试和持续集成/持续部署(CI/CD)是如何工作的,怎样才能标准化这些流程?

  • 我们如何确保维护最高标准的安全性和合规性?

Matt Walsh拍摄的照片,来源于 Unsplash

这绝不是一个完整的列表,但足以识别出一个模式:

许多上述问题无法在单个小组的范围内解答,而必须在项目层面上解决。这意味着我们需要在小组之上有一个更高层次的监督,以推动标准化,并且需要建立一个为小组服务的治理和指导流程。该流程并非旨在减缓开发活动,但如果正确应用,可以帮助减少波动、优化输出并加快开发速度。总体而言,该流程为我们提供了开发解决方案的护栏。

另一个重要的考虑因素是是否在前期指定要求或在实施后记录要求。然而,实施前指定要求在执行效率方面具有显著优势。

详细说明是什么?

详细说明是定义解决方案提案的过程,通常会导致一个轻量级的规格文档,供开发小组执行。产品负责人通过组建详细说明团队来领导详细说明,收集业务级要求,发现和转化潜在的现有遗留功能,定义用户角色和用户故事,测试程序,并与指定的架构师一起制定解决方案规格文档。

详细说明以架构评审委员会(ARB)的成功批准结束,这就是我提到的治理层。

需求管理的步骤

由作者提供的图片

  1. 在 IBM,我们通常使用 Aha!或 Jira 来收集漏斗顶部的需求。虽然短期企业目标可以显著影响需求,但我们的优先排序过程主要围绕企业战略、核心转型原则和对提升用户体验的重视。

  2. 一个详细说明通常始于已接受的需求,由工作流领导者优先排序并分配给匹配的产品负责人

  3. 产品负责人组建详细说明团队(详见下文)。

  4. 根据复杂性,进行一系列会议以研究、收集需求、定义角色、编写用户故事、提出架构决策、指定数据模型和 API、提供线框图,并收集合理的实施细节。这通常在较短的时间内发生,几周时间被认为是最大期限,但可能会有例外。

  5. 收集和总结相关信息到规格说明文档中,该文档的大小可以有所不同(见下文),并提交给架构评审委员会(ARB)。ARB 作为一个检查点,审查提案,验证架构一致性,并总体测试详细设计是否妥善进行。详细设计在获得 ARB 批准后成功结束。

  6. 产品负责人将规格说明文档拆分为 Jira 故事和任务,这些任务分配给开发小组的成员执行。

  7. 开发和测试在小组中进行。

  8. 功能、能力或集成功能上线,且应用了变更管理(OCM)。

详细设计团队

产品负责人负责组建详细设计团队,该团队通常包括以下角色的代表:

  • 工作流(或投资组合)负责人:领导产品战略,定义为什么需要或必须更改某个功能。

  • 产品负责人:负责功能、能力或集成,负责召集详细设计团队并推动详细设计。产品负责人定义功能的具体内容。

  • 架构师:确保解决方案提案在产品/平台范围内以及相关的上游和下游系统中的技术适配。架构师定义如何实现功能,确保架构的连贯性、一致性和标准化。架构师确保功能适合更大的整体图景。

  • 主题专家(SMEs):了解待实现或现有解决方案的人,能够指导团队处理相关细节。

  • 设计与研究:定义解决方案预期覆盖的各种场景的用户体验。这包括用户体验研究,以了解用户理想的工作方式以及设计线框图的制作。

  • 变更管理(OCM):在功能上线前后,提供必要的步骤以通知和启用用户。

基于 T 恤尺码的高效规格说明

在 IBM,我们使用粗略的 T 恤尺码来区分我们近期转型项目中的解决方案提案的大小:

  • 小型 — 低工作量,低复杂度,通常附加到现有功能,开发工作大约需要一个冲刺(两周)。这种类型的规格说明作为Jira Epic提交给 ARB,且没有正式结构。

  • 中型 — 中等规模的投入和复杂度,可以是一个小的新功能,最多两次冲刺(四周)的开发工作。这类规格应作为“架构决策简报”(ADB)文档提交,内容不应超过五页,通常描述待解决的问题、解决方案以及一些实施细节。

  • 大型 — 高投入和复杂度,通常是一个新的功能或与两个以上冲刺的集成。这是我们处理的最复杂的规格文档,大小可以在 10 到 40 页之间变化。最相关的方面是找到时间投入与“足够好”之间的正确平衡。规格应创建一个适当的架构框架用于实施,并提供一致性、架构的凝聚性和标准化。同时,它还应为开发团队提供足够的指导,以实现功能。

规格文档结构

为了提供对这种规格文档结构的理解,以下是一个通用的提纲:

  • 介绍和执行摘要 — 本节提供了功能概述,并从执行层面描述“为什么”

  • 术语 — 包含任何使用的缩写描述的术语表。

  • 业务需求、用户故事、角色和场景 — 本章代表“什么”。它从一个高层次的需求集合开始,深入到具体的用户故事,最终描述产品中的必要场景,包括涉及角色的流程图。这是将原始需求与实施细节对接的最相关部分,创建了对将交付内容的共同理解。

  • 架构 — 本章及接下来的三章代表了“如何”,并用技术术语描述解决方案,考虑现有的产品、平台及其生态系统。

  • 对象模型 — 本节包括任何新对象或对现有对象的更改,并处理安全性、合规性和保留问题,如记录共享、字段可见性或字段历史跟踪。

  • 用户界面原型 — 针对上述每个用户故事,设计会创建并提供一组用户界面原型。在规格文档交付时,这些原型不一定需要像素完美,但需要清晰地勾勒出用户界面如何支持指定的流程以及新页面如何融入产品的信息架构。

  • 实施细节 — 这里定义如何配置或开发实施的具体方面,以支持开发团队。该章的范围可能大相径庭,实际上取决于编制时间和开发团队的技能深度。

  • 未来的考虑和开放话题 — 本章区分了规范中排除的内容以及可能在以后发生的内容。

总结

我们观察到,前期花时间进行特性规范和创建文档并不一定与敏捷方法论相冲突。关键在于追求“足够好”。

如果我们把软件开发视作一个涉及多个迭代步骤的旅程,那么规范可以看作是目的地的一个模糊愿景。了解我们要去哪里是至关重要的,而不是盲目前进。

规范还充当了工作流负责人、产品负责人、架构师、设计、OCM 和开发团队之间关于交付内容的合同。它建立了一个“足够好”的相互理解水平,并确保每个人都在同一页面上。

图片由Antonio Janeski提供,来源于Unsplash

如果你觉得阐述过程有点太昂贵,请记住,你也可以分阶段提交规范文档。这在 ARB 中是很常见的情况,它允许开发团队在项目仍在阐述中时开始处理项目的一部分。

值得注意的是,IBM 的 ARB 中总是有指定的架构师和产品负责人参与。这在准备 ARB 提交时非常有帮助,这意味着提交不是意外的,已经与更广泛的团队对齐。我们的一次性通过 ARB 提交的成功率超过 98%,这表明 ARB 只是一个检查点,详细阐述过程在此之前已经很好地完成了。可以说,ARB 只是阐述的催化剂。在非常少见的情况下,批准会被拒绝,但只有当明显没有适当的阐述、解决方案考虑不周或缺乏关键细节时才会发生。这只会发生在提案的产品负责人与更广泛的团队未连接时。

因此,带有 ARB 批准作为检查点的详细阐述过程为团队创建了正确的心态,以便为敏捷执行指定一个“足够好”的特性。

谢谢。

关于托马斯·雷内克 — 在过去 5 年中担任多个关键 IBM 内部转型项目的首席架构师期间,我有独特的机会共同领导并影响 IBM 一些最复杂、最全面和最具影响力的业务转型,其中包括支持、销售和业务合作伙伴生态系统的转型。以合理的水平指定能力在所有这些项目中都是一个常见的热点话题。

https://medium.com/@thomas.reinecke/membership

像数据科学家一样解决神秘盒子

原文:towardsdatascience.com/solve-a-mystery-box-like-a-data-scientist-f9ee9570ba52

获取数据,训练 ViT,最小化问题;实在是过度处理

Dennis BakhuisTowards Data Science Dennis Bakhuis

·发表于 Towards Data Science ·阅读时间 17 分钟·2023 年 1 月 13 日

--

图 1:一个神秘盒子、数据收集过程,以及最终打开的锁。

当一个数据科学家得到一个以盒子形式出现的谜题时,会发生什么?当然,他会(尝试)将其作为数据问题来解决。在这篇文章中,我将描述整个过程,坦白说,这并不像我想象的那么容易。与许多问题一样,你可能会完全迷失,只有通过与几个朋友交谈,我才得以重新回到正轨。

作为一个数据科学家,我喜欢以数据的方式来解决这个问题。我意识到这种方法远非最明显的解决方案。但这是一个非常有趣的尝试。收集过多的数据,训练一个变压器模型从视频中提取值,最后使用 minimizer 找到解决方案。本文是这个(大部分)有趣旅程的总结!

我将这篇文章分成了几个(对我而言)逻辑步骤。你可以随意跳过你喜欢的部分:

  1. 你提到的这个“神秘盒子”是什么?

  2. 正式的问题描述

  3. 收集所需的数据

  4. 处理数据的善意(标记、训练、推断)

  5. 分析数据集并找到目标

  6. 前往位置

除非在单独的说明中另有说明(在本文中没有),否则本文中的所有图像均由我拍摄或生成。

此项目的所有代码都在 Notebooks 中共享,并且可以在我的 Github 账号上找到。如果你有任何评论或问题,我很乐意通过 LinkedIn听取。

你提到的这个“神秘盒子”是什么?

在我生日那天,我的朋友 Sander 给了我一个神秘的盒子,他脸上带着大大的笑容,这将让我忙上一段时间。

神秘盒子立刻看起来非常引人入胜(见图 1)。显然,这个盒子是由 Sander 亲自制作的,因为它具有非常明显的熔融沉积建模(FDM)打印图案。正面展示了一个常见的 LCD 屏幕和一个红色按钮。左侧有一个相对较大的挂锁。这是一个需要四位数字代码才能打开的挂锁。挂锁防止盒子底部的滑盖打开。右侧是一个小的多余空间,用来放置一个 9 伏特的电池。电池外置是相当聪明的,这样当电量低时可以更换。

图 2:这是神秘盒子:一个带有屏幕、按钮和挂锁的 3D 打印设备。

从创造者的角度来看,神秘盒子有一个相当有趣的设计。顶面(如图 2 所示)在打印过程中作为基础使用,因为这个面比其他面粗糙得多。这使得多余的电池架也可以在没有支撑的情况下打印。尽管如此,我还是不完全确定 LCD 屏幕的孔是如何创建的。最简单的方法是打印轻质支撑,然后切除。用于挂锁的台阶上的打印支撑残留物仍然可见。总体来说,这是一个不错的项目,可以让你的打印机运行至少 8 小时。

红色按钮是一种切换按键。按下时你会听到它卡扣的声音,LCD 屏幕会亮起。几秒钟后,盒子会用“Hi!!!”向你打招呼,这是在流体物理研究组的经典搞笑方式。接下来几条屏幕上的信息是荷兰语。有关屏幕的概述请参见图 3。以下是一些快速翻译:

  1. Hi!!! -> <低音调的声音> Hi </低音调的声音>

  2. Van harte gefeliciteerd! -> 真诚的祝贺!

  3. Vind de twee punten en ga -> 找到两个坐标并前往

  4. naar het midden van de punten… -> 到坐标的中心…

  5. Zoekt gps… Een moment… -> 正在搜索 GPS… 请稍等…

  6. Afstand(p1)-Afstand(p2)=1048m -> 距离(p1)-距离(p2)=1048m

图 3:打开设备会显示各种荷兰语信息,包括几个提示。

在两个提示屏幕之后,系统正在忙于搜索 GPS 信号。这个过程,即所谓的“第一次定位时间”(TTFF),可能很长,因为设备总是需要进行所谓的“冷启动”。根据标准,这可能需要长达五分钟。

最后,在 GPS 固定后,盒子进入主操作界面,显示它唯一的输出:点 1 到点 2 的距离差,以米为单位的整数值。

桑德还对单位做了一些额外的说明。该单位输出一个值,该值是两距离d1d2之间的差值。这些距离是当前箱子位置与两个未知坐标之间的距离。目标是找到正好位于这两个未知坐标之间的坐标。

这听起来很简单,但让我们看看更正式的描述。

正式问题描述

谜题的目标是将其带到一个特定的位置。如果这个位置是正确的,箱子将显示出密码锁的代码。这个问题的挑战在于神秘箱子不会显示到那个特定位置的距离。

目标位置G正好位于两个未知坐标p1p2之间的中点。距离d1d2是当前箱子位置B与坐标p1p2之间的距离。箱子在屏幕上显示的是d1d2之间的差值,我们称之为A。图 4 展示了问题的简化示意图。

图 4:一个简单的问题概述。目标G正好位于未知坐标p1p2之间。箱子返回的唯一信息是A,即距离d1d2之间的差值。距离d1d2分别是当前箱子位置B与坐标p1p2之间的距离。

图 4 中的示意图显示我们有一个向量问题。如果我们假设一个二维问题空间,我们需要每个坐标两个值来唯一标识问题空间中的一个位置。

存在一个点,使得差值A为零。这是当距离d1等于d2时。在二维空间中,这会导致一条垂直于p1p2之间线的线(见图 5)。所以从技术上讲,如果我们找到两个不同的位置,使得A为零,我们可以连接这些点并知道目标在那条线上。

图 5:当A等于零时,d1d2相等。存在一条与目标坐标垂直的线,使得A等于零。如果我们找到两个不同的位置,使得A为零,我们可以找到这条线。

要解这个方程,我们可以尝试解一个方程组(四个未知数需要四个方程),但我们的问题是非线性的。这会在试图孤立方程中的项时产生一些复杂的方程(方程 1)。在方程中,xy是当前箱子位置坐标的两个维度。也许不是最优雅的,但这个方程可以通过最小化误差函数来数值求解。这正是我们在机器学习中使用梯度下降和更现代的前向前向方法所做的。

方程 1:简化情况下的距离差方程。

直到现在,我们描述的问题仅在平面地球学会内有效。我们用直线来表示距离,这意味着我们需要在球面世界中挖掘隧道。与其在平面上计算距离,我们需要在球面上计算距离。我已经更新了图 6 中的问题示意图。

直到现在,我们描述的问题仅在平面地球学会内有效。

图 6:我们并不生活在平面地球上。问题实际上是在球面上。

计算球面上两点之间的距离远非简单。这涉及应用大圆距离公式,这看起来相当复杂。幸运的是,人们发明了一种新的三角函数,使其看起来更为优雅:哈弗辛公式。如果你喜欢对这个问题的深入阅读,这里有一篇关于地下数学的好文章。

使用哈弗辛公式来计算单个距离是相当可行的,然而,我们的神秘箱子输出的是两个距离 A 之间的差值。这使得最终的方程式极其复杂,我认为从数值上解决这个方程最为合理。

但在我们解决任何问题之前,我们需要获取数据。在下一部分,我将详细描述设置过程。

收集所需的数据

当尝试将物理谜题作为数据问题来解决时,最大的问题是很可能没有数据。这意味着我们需要收集自己的数据。根据我的物理学经验,收集数据并非易事。很多事情可能会(并且可能确实会)出错。老实说,为了这个项目,我可能已经骑了超过 80 公里。

很多事情可能会(且可能确实会)出现问题。

我们可以通过多种方式来收集数据。首先,我们可以骑自行车 🚴(我们在 🇳🇱,所以是的,我们🚴 😃),并停下来记录当前的纬度和经度以及箱子 A 的输出。从技术上讲,几个点就足够了,但这样做没什么趣味。

与其做这些手动工作,为什么不做一些经典的过度工程,获取神秘箱输出的视频,并将其与 GPS 记录器的数据匹配。为此,我们需要制作一种装置,将相机固定在箱子 LCD 屏幕前面。我想到的最直接的方法是将手机绑在某种容器上,这样可以在相机上获得清晰的图像。一个塑料盒子刚好有合适的尺寸,因此我可以将箱子放在盒子里,制造一些箱子套箱(我们要更深入)。相机需要稳固,以便相机和 LCD 屏幕之间的移动最小化。这需要一些严肃的胶带。

现在我们有了记录箱子输出为电影的解决方案。我手机的常规相机应用效果不好,所以我使用了一个名为HD Camera的应用程序。要获取当前位置,我们需要一个 GPS 记录器。为此,我从 Playstore 下载了GPS logger。安装了这两个应用程序后,我准备好收集一些数据。整个设置如图 7 所示。

图 7:数据收集的设置。神秘箱子被盒子套箱。盒子上面绑着一个手机,以便相机能清晰地看到 LCD 屏幕。手机上有两个活动应用,一个相机应用和一个 GPS 记录器。GPS 记录器处于浮动应用模式,以便保持活动状态,不被手机的省电设置关闭。

我已经提到过在做实验时可能出错的很多事情。以下是我犯下的许多错误的自白:

  1. 在第一次骑行时电池耗尽了。当手机在电量耗尽时仍在录制时,你会丢失录音。这让我为设备添加了一个额外的移动电源。( 🚴 ~12 km)。

  2. 下一次骑行时我的存储满了。我直到骑行快结束时才看到错误信息。( 🚴 ~10 km)。

  3. 在另一次骑行中,由于阳光我不喜欢光线。( 🚴 ~9 km)。

  4. 现在我喜欢这些影像,但不知为何没有 GPS 数据。( 🚴 ~9 km)。

  5. 再次,没有 GPS 数据。但我怀疑是一些电池节省问题。( 🚴 ~9 km)。

  6. 我把 GPS 记录器设置为前台,HD 相机设置为后台。现在我有了 GPS 数据,但不知为何,相机应用停止了。( 🚴 ~9 km)。

  7. 在将 GPS 记录器应用设置为浮动应用程序(一些多任务魔法)后,我得到了 GPS 和图像数据。( 🚴 ~9 km)。

  8. 我删除了我的代码库,包括所有录音。( 🚴 ~9 km)。

图 8:我自行车箱中的设置。

所以总的来说,我骑行了将近 80 公里,获取了有用的影像和 GPS 数据。接下来,我们将深入研究这两个数据源,并将它们结合起来,以便我们有* A = f(x,y)*。

处理数据的好处

现在我们有了两个数据源,我们需要将它们处理成有用的东西。我们将首先把 GPS 数据转换为默认的 DataFrame。接下来,我们还将把 4.7GB 的视频转换为 A 值的表格。这将是一些相当不错的数据缩减 😃。

转换 GPS 记录数据

当手机上的相机记录 LCD 屏幕的变化值时,GPS 记录器应用程序记录了当前的纬度和经度值。这些值以 1Hz 的记录速率存储在一个所谓的 GPX 文件中,Python 中有一个巧妙的库可以处理这些文件。它方便地叫做 gpxpy,安装后数据可以轻松加载:

代码 1:导入 GPS 数据并创建 DataFrame。简直是小菜一碟!

输出 1:新创建数据集的示例。

现在我们有一个包含 2133 个测量值的 DataFrame,其中包括纬度、经度、海拔和时间。为了检查这些坐标是否合理,我们可以简单地使用 Matplotlib 创建一个参数图。然而,Python 还有另一个专门用于地图的优秀库,叫做 folium。让我们可视化一下我骑行的路线:

代码 2:使用 Folium 可视化 GPS 坐标。

输出 2:美丽的恩斯赫德地图和我骑行的确切路线。

在美丽的 Folium 地图上,我们使用了“tamen watercolor”图块,正好显示了我骑行以收集数据的路径。在将 DataFrame 存储到磁盘之前,我们需要修正时间列中的时区。目前,时区设置为“z”,我不确定这是什么。为了避免任何复杂性,我们需要本地化时间。

代码 3:修正时间并将 DataFrame 存储为 Parquet 文件。

GPS 数据已经准备好使用了。接下来,我们需要将视频镜头处理成有用的内容。

创建图像检测器的训练集

我们有一个视频展示了骑行过程中神秘盒子的所有输出。我们需要从每一帧中提取 A 值及其对应的时间。利用这些时间,我们可以将 A 值与其 GPS 位置链接起来。为了提取 A 值,我们将训练一个 基于 DONUT 的模型。这是一个端到端的模型,不需要任何光学字符识别(OCR)。为了训练模型,我们需要一个标注好的数据集。首先,让我们使用 OpenCV 检查视频数据。

代码 4:将视频作为流导入并检查帧率。

该视频的帧率为每秒 30 帧。视频的持续时间大约为 35 分钟,因此总帧数会超过 63k。这有点多,特别是因为大多数 Arduino 的 GPS 传感器更新频率大于 1Hz。因此,我们只选择每秒一帧,并将它们放入一个常规列表中。

代码 5:以每秒 1 帧的帧率从视频中提取帧。

输出 3:视频中的单帧(左)和最终预处理的帧(右)。

我们已经提取了 2119 帧,这已经是从最初的 63k 帧中减少了相当多。当查看输出 3(左侧)的帧时,我们发现帧中的许多像素对我们并不特别有趣。在预处理过程中,我们将裁剪图像,仅保留 A 值。我们还会将图像转换为黑白,这可能有助于后续的对比度问题。

代码 6:预处理图像,以便我们孤立 A 值。

现在我们只需要保存列表。这是一个相对容易并行化的任务,因为它不依赖于之前的输入,因此我们使用[joblib](https://joblib.readthedocs.io/en/latest/parallel.html)来完成这一步。

代码 7:存储图像,当然要并行处理!

我们现在有了一个数据集,但它尚未标记。为了标记,我创建了一个工具,用于在 Jupyter Lab(或 Notebook)中标记数据,称为[Pigeon-XT](https://github.com/dennisbakhuis/pigeonXT)。这是 Anastasis Germanidis 创建的 Pigeon 标记工具的扩展版本。我们将标记 250 个示例,这花了我大约 15 分钟。

代码 8:在我们的 Jupyter Notebook 中舒适地标记数据。

现在我们有了一个标记的数据集。虽然它很小,大约占完整数据集的 10%,但我们应该能够训练一个模型,用来填补数据集的缺失部分。为了让我们的工作更轻松,我们可以使用🤗 Huggingface 的[datasets](https://github.com/huggingface/datasets)库。我们可以通过一些小调整将数据导入为 Imagefolder 数据集。

代码 9:创建一个 Huggingface 数据集对象。

输出 4:数据集对象使数据处理变得非常容易。也许使用其.map()函数进行转换会更好。

最后但同样重要的是,我们需要保存数据集。值得一提的是,可以在 Huggingface 中心共享你的数据集。使用[huggingface_hub](https://github.com/huggingface/huggingface_hub)包非常简单。为此,你还需要一个中心的账户。在中心共享的神奇之处在于,你现在可以下载这个数据集,而无需进行我上面做的所有处理。

代码 10:保存数据集并将其推送到 Huggingface 中心。

现在我们拥有了训练模型以推断缺失标签的所有条件。让我们开始训练吧🔥🔥🔥!

训练一个🍩 DONUT 模型

我们终于可以开始训练我们的模型了。我们将训练一个基于DONUT的模型,该模型由Naver AI Lab创建。DONUT 是一个真正的端到端模型。你输入一张图像,它将输出一个包含键值对的 JSON 对象。最令人惊叹的是,它可以在没有 OCR 的情况下完成这一切。之前的最新方法,如LayoutLM,使用了两步法:首先使用 OCR 提取所有文本及其位置,其次,将图像和文本数据输入模型以提取信息。

从文档中提取信息的问题在于,很多信息被编码在布局中。人脑能够理解以标题、表格或标题的形式呈现的数据之间的关系。使用 OCR 方法,这些信息会丢失。像 LayoutLM 这样的方式利用图像恢复布局信息。DONUT 可以一次性完成这项工作,并且内置了 OCR。从我所看到的,它的效果还不错,但对文档中的变化非常敏感。当文档有微小的布局变化,但要提取的信息仍然相同时,DONUT 容易失败。

我们的图像实际上不是文档,但它们文本非常清晰,从布局的角度来看也非常一致。因此,DONUT 将是一个极好的(甚至有些过度)提取 A 值的方法。我按照 CloveAI 团队在他们的 代码库 中所做的相同步骤进行操作。Philipp Schmid 写了一篇 很棒的文章 介绍了如何训练自己的 DONUT 类型模型。首先,我们需要为模型的特定 DONUT 输入准备数据集。

代码 11:将 JSON 对象转换为 DONUT 期望的令牌。

现在数据已符合 DONUT 的预期格式,我们需要对数据进行分词。这是一个与 GPT 和 BERT 等转换器语言模型共同的步骤,但现在也用于视觉转换器(ViT)中的图像。

代码 12:为模型输入对数据进行分词。

现在我们已经准备好进行实际训练了 🔥。如今,这非常简单,因为你不再需要编写训练循环了。虽然我起初有些难过,因为编写循环感觉很酷,但它其实非常重复。Huggingface 提供了训练器对象,它非常适合这项工作。

代码 13:训练循环的完整代码已经简化了很多!

输出 5:训练进行中。

在测试数据集的评估后,我们得到了 0.007 的评估损失,这相当不错。在我们 38 个测试样本中,有 37 个被正确检测出来。唯一一个不正确的是少了一个数字:059 而不是 1059。37/38 的准确率为 97%,这可能足以标记完整的集合。在我们开始推断缺失值之前,让我们先保存模型并将其推送到中心。

代码 14:保存模型并将其推送到中心。

现在让我们对缺失的标签进行一些推断。

使用模型推断缺失数据

最终,我们可以使用我们的模型来推断标签。图像已经准备好,所以我们可以直接使用它们。每个帧的时间以秒为单位编码在其文件名中。由于帧率不是精确的每秒 30 帧,因此会有小的偏差,但差异会很小。让我们首先创建一个函数来为我们进行推断:

代码 15:推断图像的代码。

我们现在可以对每张图片逐一进行推断,但这将花费大约一个小时。让我们并行处理,但这远非简单。对于简单的任务,joblib 很棒。然而,在使用模型时,为每次迭代序列化大型模型会比逐个处理更慢。为了解决这个问题,我之前编写了 [tqdm_batch](https://github.com/dennisbakhuis/tqdm_batch)

并行作业并非易事。阅读我的 文章🏃🏻💨!

代码 16:并行化推断步骤,并将工作分配给四个工作者。

输出 6:使用 tqdm_batch 运行 4 个工作者并行处理。

现在我们有了来自盒子的 A 值,我们可以构建最终的数据集并将其与 GPS 数据结合起来。像往常一样,预测并不完美,我们需要做一些调整。作为数据科学家,你实际上是一个数据按摩师。

代码 15:对数据集进行基本清理。

几乎完成了,唯一的问题是错误检测到的 A 值。这些可以使用滚动滤波器进行过滤。我们将删除与移动平均值差异超过 5 米(任意小数)的值,并用剩余值进行插值。

代码 16:使用移动平均过滤器和插值来过滤噪声数据。

输出 7:应用过滤器后的最终结果。相当不错!

这个过滤效果相当不错。现在我们准备将这个数据集与之前记录的 GPS 数据结合起来。

结合图像和 GPS 数据

将 A 值与 GPS 数据结合应该不成问题。对于 GPS 数据集中的每一行,我们将寻找时间上最接近的匹配,并使用该行的 A 值。

代码 17:将 A 值添加到 GPS 数据集中。

就这些了!付出了很多努力,可能其实并不必要。但我们使用了机器学习,这才是最重要的!

我们使用了机器学习,这才是最重要的!

接下来,我们将使用这个数据集并尝试找到目标坐标。

分析数据集并找到目标

如前所述,我们将尝试找到两个未知位置 p1p2。如果我们知道这两个点,我们也将知道目标位置,它应该正好在这两个点之间。为了找到这些位置,我们将使用 [minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html#scipy.optimize.minimize) 来自 [Scipy](https://scipy.org/)。虽然我可以自己编写 Haversine 函数,但我将直接使用 [haversine](https://github.com/mapado/haversine) 包。要使用 minimize,我们需要编写一个误差函数。这个函数将计算已知的 A 和计算出的 A 之间的差异,并对数据集中的所有值求绝对差异的总和。最小化函数应该最小化这个误差。

代码 18:让我们最小化这个问题。

输出 8:最终解决方案(绿色)正好位于点 p1 和 p2(蓝色)之间。目标也在我找到的零点的垂直线上。

就是这样,我找到了位置。目标正好位于点 p1 和 p2 之间。这个点也连接到我之前找到的零点之间的垂直线上。但最大的提示是目标位置是一家 BBQ 餐厅。桑德和我都很喜欢汉堡,而这个地方有很棒的选择。

输出 9:我找到的坐标与实际目标之间的差异是 93 米。

实际点与我找到的点之间的偏差是 93 米。这可能有很多原因。首先,提取的 A 值只有整数。这确实有影响,但我怀疑这只是一个较小的影响,平均在较大的数据集上应该可以忽略。当然,过滤(平滑)也有影响。也许故意添加了一些噪声,不确定我是否能注意到。总之,我对结果相当满意。

前往位置

花了相当长的时间,但现在我终于确定了位置。我开车去了 Marcook,一家位于恩斯赫德外的烧烤餐厅。如果我在那个位置打开神秘盒子,它应该会显示解锁其谜团的代码。

图 9:神秘盒子解锁了所有的谜团。

的确,等待了几分钟以获得 GPS 定位后,盒子几乎立刻回应了‘proficiat’。祝贺你走到这一步。设备的下一个屏幕显示代码:7631。

老实说,这不是一个简单的挑战。但这是一次有趣的旅程,最后我们会吃一些汉堡。几乎没什么能比这更好。

此项目的所有代码都在 Github 上。大部分较小的数据集包含在 Git 仓库中,但较大的视频通过我的 Dropbox 共享。用于 ViT 训练的 dataset、DONUT 处理器和 DONUT 模型也在 🤗 Huggingface hub 上共享。如果你有任何问题,欢迎通过 LinkedIn 联系我。

最后但同样重要的是,我要感谢 🙏 桑德 给予的有趣体验。对不起我花了这么长时间,但我想我有点过度了。这确实很有趣!汉堡?

揭开锯齿状 COVID 图表的谜团

原文:towardsdatascience.com/solve-the-mystery-of-the-serrated-covid-chart-b0b517b224ef

使用 pandas 将数据下采样到合适的分辨率

Lee VaughanTowards Data Science Lee Vaughan

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 9 月 12 日

--

DreamShaper_v7_A_computer_monitor_displaying_a_chart_with_a_jagged_blue_line (作者 & Leonardo AI)

在 COVID-19 疫情的第一年,疾病的死亡人数引起了许多争议。争议的问题包括由于缺乏检测导致的早期低估、医院外死亡人数未被记录,以及区分 COVID-19 的死亡和伴随 COVID-19 的死亡 [1][2]。

最终,所有的麻烦,以及大家的巨大不幸,疫情迅速变得政治化。党派评论员对每一条数据都跃跃欲试,寻找可以扭曲数据以谋取自己利益的方式。确认偏误猖獗。如果你当时在社交媒体上,可能见过质疑官方图表和数据准确性的帖子。

在这个 快速成功的数据科学 项目中,我们将查看一张当时出现在我 Facebook 动态中的特定图表。该图表记录了疫情第一年美国 COVID-19 的死亡人数,并显示出明显的锯齿状或“锯齿形”特征。

疫情第一年美国 COVID-19 死亡人数(作者来自《大西洋月刊》的“The COVID Tracking Project” [3])

曲线的振荡频率很高,疾病是否以这种方式进展令人怀疑。虽然一些人认为这证明了 COVID 死亡统计明显错误且不可信,但我们这些拥有数据科学技能的人很快就揭开了这个被夸大的谜团。

数据集

我们将使用的数据是作为“COVID 跟踪项目”的一部分收集的,数据来源于大西洋月刊 [3]。它包含了从 2020 年 3 月 3 日到 2021 年 3 月 7 日的 COVID-19 统计数据。为了减少数据集的大小,我只下载了德克萨斯州的数据,并将其保存为 CSV 文件,文件在这个 Gist 中。

你可以在 这里 找到原始数据集,在 这里 找到数据的许可信息。

安装库

除了 Python,我们还需要 pandas 库。你可以通过以下两种方式之一安装它:

conda install pandas

或者

pip install pandas

pandas 的一个好处是它内置了绘图和处理时间序列的功能,即按时间顺序排列的数据点。Python 和 pandas 都将日期和时间视为特殊对象,这些对象“了解”公历、性别(60 基数)时间系统、时区、夏令时、闰年等的机制。

原生 Python 通过其 [datetime](https://docs.python.org/3/library/datetime.html) 模块支持时间序列。Pandas 的 datetime 功能 基于 NumPy 的 datetime64timedelta64 数据类型。通过将“字符串”日期转换为“真实”日期,我们可以做一些有用的事情,如提取星期几或对数据进行按周或按月平均。

代码

以下代码在 JupyterLab 中编写,按单元格描述。

导入库和加载数据

导入 pandas 后,我们将 CSV 文件加载到 DataFrame 中,只保留“date”和“mortalities”列。然后,我们使用 pandas 的 to_datetime() 方法将“date”列转换为 datetime,排序,并将其设置为 DataFrame 的索引。

import pandas as pd

df = pd.read_csv('https://bit.ly/3ZgrmW0', 
                 usecols=['date', 'mortalities'])
df.date = pd.to_datetime(df.date)
df = df.sort_values('date')
df = df.set_index('date')
df.tail()

绘制初始数据

Pandas 的绘图功能有限,但对于数据探索和“快速查看”分析来说足够了。

df.plot();

# Optional code to save the figure:
# fig = df.plot().get_figure();
# fig.savefig('file_name.png',  bbox_inches='tight', dpi=600)

初始德克萨斯数据集(作者提供的图片)

这个图表呈现了与国家数据相同的“波动性”。它还包括了在 7 月底附近的一个明显峰值。我们在调查振荡之前先看看这个峰值。

处理峰值

由于这个峰值显然是一个最大值,我们可以很容易地通过使用 max()idxmax() 方法分别检索其值和对应的日期索引。

print(f"Max. Value: {df['mortalities'].max()}")
print(f"Date: {df['mortalities'].idxmax()}")

这很可能是一个异常值,特别是考虑到 疾病控制中心 (CDC) 在这一天仅记录了 239 人死亡,这与相邻数据更一致。我们将使用 CDC 的数据。要更改 DataFrame,我们将应用 .loc 索引器,并传递日期和列名。

# Set aberrant spike at 2020-7-27 to CDC value of 239 deaths:
df.loc['2020-7-27', 'mortalities'] = 239  
df.plot();

修复了尖峰的德克萨斯数据(图像由作者提供)

看起来更好。现在让我们评估曲线的锯齿状特性。从视觉上看,每个月大约有 4-5 次波动,暗示了每周的频率。

检查每周数据

为了更详细地研究这一问题,我们将按星期几绘制数据的随机子集。我们将首先制作一个名为“df_weekdays”的 DataFrame 复制,并添加一列用于星期几。然后我们将绘制约两周的数据,范围为(任意)索引 103–120。

# Examine values by weekday:
df_weekdays = df.copy()
df_weekdays['weekdays'] = df.index.day_name()

df_weekdays.iloc[103:120].plot(figsize=(10, 5), rot=90, x='weekdays');

按星期几绘制的德克萨斯数据子集(图像由作者提供)

计数在周末期间和之后似乎下降。让我们使用表格格式进一步调查这一点。我们将查看 3 周的间隔,并在打印输出中突出显示周一。

# Highlight Mondays in the DataFrame printout:
df_weekdays = df_weekdays.iloc[90:115]
df_weekdays.style.apply(lambda x: ['background: lightgrey' 
                                   if x.weekdays == 'Monday'
                                   else '' for i in x], axis=1)

最低报告的死亡人数始终发生在周一,周日的结果也似乎被抑制。这表明周末存在报告问题。

确实,这一问题已被确认为艾尔伯特·爱因斯坦医学院和约翰霍普金斯公共卫生学院的研究人员所发现[4]。他们发现,这些波动仅出现在死亡日期反映报告日期的数据集中。在追溯事件日期的数据集中,这些波动是不存在的。

从天到周的降采样

由于报告问题,这些数据的适当分辨率是每周,而不是每日。为了以每周间隔绘制数据,我们需要使用 pandas 的 resample() 方法从较高频率降采样到较低频率。由于必须将多个样本合并为一个,resample() 方法通常会链式调用用于聚合数据的方法,如下图所示。

有用的 pandas 聚合方法(摘自“Python 工具科学家”[5])

再次,由于报告问题影响了每日计数,但在每周基础上进行修正,因此将数据从每日降采样到每周应当可以合并低报告和高报告,并平滑曲线。我们将通过将 resample() 方法的参数设置为 W 并链式调用 sum() 聚合方法来实现这一点,从而求和每日值。除了 W,其他有用的时间序列频率见下表。

有用的 pandas 时间序列频率(摘自“Python 工具科学家”[5])

# Resample weekly to remove serrations:
df.resample('W').sum().plot(grid=True);

将德克萨斯数据重新采样到每周频率(图像由作者提供)

将报告偏差“折叠”到新的降采样时间序列中,曲线看起来更平滑,正如我们所期望的那样。

总结

在这个项目中,我们应用了数据科学技术来解释历史 COVID-19 死亡率图表中奇怪的锯齿状特征。这些波动被一些人用来质疑死亡率数据的真实性。

我们发现这些波动似乎反映了每周报告的节奏,指向了病例报告中的偏见行为。在建议其他机制之前,例如周末医院护理质量或政府干预报告,应该考虑像这样的操作性解释。

我们使用 pandas 完成了这个项目,pandas 是 Python 的主要数据分析包。Pandas 非常适合像这样的“快速查看”分析。除了其类似电子表格的功能外,它还包括用于绘图和处理日期的内置工具。

我们使用的过程很简单,但这正是要点。编织一个阴谋论可能比揭穿一个阴谋论需要更多的努力。我在企业界的经历也类似。我们有时会争论 3 周是否执行一个任务,而这个任务如果有人坐下来做的话,仅需 3 小时就能完成!

谢谢!

感谢阅读,关注我获取更多快速成功数据科学项目。

引用

  1. Lang, Katherine, 2022 年 3 月 11 日,我们是否高估了 COVID-19 死亡人数? (medicalnewstoday.com)

  2. Fichera, Angelo, 2021 年 4 月 2 日,缺陷报告助长了关于 COVID-19 死亡人数的错误说法 — FactCheck.org

  3. 大西洋的 COVID 跟踪项目

  4. Bergman, A., Sella, Y., Agre, P., & Casadevall, A. (2020), “美国 COVID-19 发病率和死亡率数据中的波动反映了诊断和报告因素,”MSystems, 5(4), doi.org/10.1128/mSystems.00544-20

  5. Vaughan, Lee, 2023 年,Python 工具:Anaconda、JupyterLab 和 Python 科学库的入门,No Starch Press, 旧金山。

使用强化学习解决 Leetcode 问题

原文:towardsdatascience.com/solving-a-leetcode-problem-using-reinforcement-learning-a-practical-introduction-to-6ec9959dd309?source=collection_archive---------8-----------------------#2023-08-29

强化学习的实用介绍

Pratik AherTowards Data Science Pratik Aher

·

关注 发布于 Towards Data Science ·7 分钟阅读·2023 年 8 月 29 日

--

最近,我遇到了一个在 leetcode 上的问题:网格中的最短路径与障碍物消除 网格中的最短路径与障碍物消除问题涉及在包含障碍物的二维网格中,从起始单元格到目标单元格寻找最短路径,你可以消除沿途最多 k 个障碍物。网格由一个“m x n”的二维数组表示,其中 0 表示空单元格,1 表示障碍物单元格。

目标是找到从起始单元格(0, 0)到目标单元格(m-1, n-1)的最短路径,途经空单元格,同时最多消除 k 个障碍。消除障碍意味着将障碍单元格(1)转换为空单元格(0),以便路径可以通过。

在网格中消除障碍的最短路径示例(图像来自作者)

当我解决这个问题时,我意识到它可以提供一个有用的框架来理解强化学习原理的实际应用。在深入探讨之前,让我们看一下传统上如何解决这个问题。

要理想地解决这个问题,我们需要从起始单元格开始进行图搜索,同时跟踪到目前为止消除的障碍数量。在每一步,我们考虑移动到一个相邻的空单元格,或在还有剩余消除次数时消除相邻的障碍单元格。最短路径是以最少的步数到达目标并且最多消除 k 个障碍的路径。我们可以使用广度优先搜索、深度优先搜索来有效找到最佳最短路径。

这里是使用 BFS 方法解决此问题的 Python 函数:

class Solution:
    def shortestPath(self, grid: List[List[int]], k: int) -> int:
        rows, cols = len(grid), len(grid[0])
        target = (rows - 1, cols - 1)

        # if we have sufficient quotas to eliminate the obstacles in the worst case,
        # then the shortest distance is the Manhattan distance
        if k >= rows + cols - 2:
            return rows + cols - 2

        # (row, col, remaining quota to eliminate obstacles)
        state = (0, 0, k)
        # (steps, state)
        queue = deque([(0, state)])
        seen = set([state])

        while queue:
            steps, (row, col, k) = queue.popleft()

            # we reach the target here
            if (row, col) == target:
                return steps

            # explore the four directions in the next step
            for new_row, new_col in [(row, col + 1), (row + 1, col), (row, col - 1), (row - 1, col)]:
                # if (new_row, new_col) is within the grid boundaries
                if (0 <= new_row < rows) and (0 <= new_col < cols):
                    new_eliminations = k - grid[new_row][new_col]
                    new_state = (new_row, new_col, new_eliminations)
                    # add the next move in the queue if it qualifies
                    if new_eliminations >= 0 and new_state not in seen:
                        seen.add(new_state)
                        queue.append((steps + 1, new_state))

        # did not reach the target
        return -1

强化学习的小介绍

强化学习(RL)是机器学习的一个领域,其中一个智能体通过奖励机制学习策略,以通过了解其环境来完成任务。我一直对强化学习感到着迷,因为我相信这一框架很 closely mirrors 人类通过经验学习的方式。这个想法是构建一个可学习的智能体,通过反复试验了解环境,以解决问题。

让我们逐一深入这些术语:

环境与智能体 Raya(图像来自作者)

  • 智能体:智能体是一个假设的实体,控制行动的过程。你可以想象一个假设的机器人,比如智能体 Raya,它从一个位置开始并探索其环境。例如,Raya 有两个可能的选择:位置 (0, 0) 向右移动,或位置 (0, 1) 向下移动,这两个位置有不同的奖励。

  • 环境:环境是我们智能体操作的背景,在这种情况下是一个二维网格。

  • 状态:状态表示玩家的当前情况。在我们的例子中,它表示玩家的当前位置和剩余的违规次数。

  • 奖励系统:奖励是我们采取某种行动后获得的分数。在这种情况下:空单元格为-1 分,到达目的地为+20 分,如果我们用完了违规次数 k,则为-10 分。

迭代过程(图像来自作者)

通过迭代过程,我们学习到一种最佳策略,这种策略使我们能够在每个时间步找到最佳行动,同时最大化总奖励。

为了找到最佳策略,我们使用了一个叫做Q 函数的东西。你可以将这个函数视为代理迄今为止所有探索的储存库。代理然后利用这些历史信息在未来做出更好的决策,从而最大化奖励。

Q 函数

Q(s, a) 代表代理在状态 s 下采取动作 a 并遵循策略 π 时可以获得的期望累计奖励。

Q 函数(作者提供的图片)

其中

  • π:代理采用的策略。

  • s:当前状态。

  • a:代理在状态 s 下采取的动作。

γ 是平衡探索和利用的折扣因子。它决定了代理对即时奖励和未来奖励的优先级。折扣因子接近 0 会使代理关注短期奖励,而折扣因子接近 1 会使代理关注长期奖励。

代理需要在利用已知的高奖励动作和探索可能带来更高奖励的未知动作之间取得平衡。使用 0 到 1 之间的折扣因子有助于防止代理陷入局部最优策略

给定一个状态,Q 函数返回一个向量,该向量为所有动作提供一个评分(作者提供的图片)

现在让我们跳到整个过程如何工作的代码部分。

这就是我们定义代理及其相关变量的方式。

奖励函数:奖励函数接受当前状态并返回该状态获得的奖励。

贝尔曼方程:

我们应如何更新 Q 表,以使每个位置和动作的值尽可能最佳?对于任意次数的迭代,代理从位置 (0, 0, k) 开始,其中 k 表示允许的违规次数。在每个时间步,代理通过随机探索或利用学到的 Q 值贪婪地移动,转移到新状态。

在到达新状态后,我们评估即时奖励,并根据贝尔曼方程更新该状态-动作对的 Q 值。这使我们能够通过将新奖励纳入每个状态-动作的历史累计奖励中,迭代地改进 Q 函数。

这里是贝尔曼方程的方程式:

Q 值方程(作者提供的图片)

这就是训练过程在代码中的样子:

构建路径:对于路径,我们利用每个网格位置的最大 Q 值来确定该位置采取的最佳行动。Q 值本质上编码了基于长期奖励在每个位置应采取的最佳行动。例如,在位置 (0,0) 的所有行动 k 中,最大 Q 值对应的行动是“1”,代表向右移动。通过在每一步贪婪地选择具有最高 Q 值的行动,我们可以构建出一条穿越网格的最优路径。

如果你运行提供的代码,你会发现它生成的路径是 RBRBBB,这确实是考虑到障碍物的最短路径之一。

这是完整代码的链接,包含在一个文件中:shortest_path_rl.py

结论

在现实世界的强化学习场景中,代理与之互动的环境可能非常庞大,仅有稀疏的奖励。

如果你通过更改 0 和 1 来改变棋盘的配置,这种硬编码的 Q 表方法将无法推广。我们的目标是训练一个代理,使其学习网格世界配置的通用表示,并能够在新的布局中找到最优路径。在下一篇文章中,我将用深度 Q 网络 (DQN) 替换硬编码的 Q 表值。DQN 是一个神经网络,它接受状态-行动组合和完整网格布局作为输入,并输出 Q 值估计。这个 DQN 应该能让代理即使在训练过程中未遇到的新网格布局中也能找到最优路径。

如果你想快速聊聊并建立联系,可以通过 LinkedIn 联系我:www.linkedin.com/in/pratikdaher/

解决所有 Apple Silicon 上的 Python 依赖问题

原文:towardsdatascience.com/solving-all-python-dependency-issues-on-apple-silicon-29a2171d1d7c

使用 pyenv 在 Mac M1 上管理多个 Python 架构的指南

David FarrugiaTowards Data Science David Farrugia

·发布于 Towards Data Science ·4 min read·2023 年 2 月 21 日

--

图片来源:Jonathan FranciscaUnsplash

我爱 MacOS。

也许是它的认证 Unix 特性,简约和优雅,或者是它的无缝集成。但我爱 MacOS。

根据我的经验,一切都会顺利进行。

在我职业生涯的开始阶段,我主要使用 Windows(同时玩弄一些 Linux 发行版)。一旦我切换到苹果,尽管这可能有些陈词滥调,但我从未回头。

那种跟随逐步教程却面临奇怪错误的日子早已一去不复返。

那种为了让某些 Python 包在 Windows 系统上特定工作而需要找寻模板代码的日子已经一去不复返。

而且这种情况经常发生。作为一名数据科学家,我不断地尝试和实验新的 Python 包。

自从 2019 年发布以来,我一直在使用我的 2019 年款 Macbook Pro 处理数字。但最近,我有机会在 2020 年款 Macbook Pro M1 上工作了一段时间。

我很兴奋。我听说 M1 芯片在性能上比 Intel 芯片强大得多。

首先,必须提到,这颗芯片真是太棒了!M1 芯片非常快,能效高,远远领先于竞争对手。从我的旧系统到 M1 的性能差异立刻显而易见。

然而,当我开始为数据科学和机器学习设置我的机器时,我很快意识到我最喜欢的 Mac 功能已经迅速消失。我无法完成最基本的操作——通过 pip 安装 Python 包。M1 芯片不断给我抛出各种依赖错误和奇怪的冲突。

这主要是由于从 x86 转向 ARM64 的切换。尽管现在的情况已经大大改善,许多开发者更新他们的软件包以支持 ARM64 系统,但仍有大量 Python 软件包尚未更新。

进入 pyenv

[## GitHub - pyenv/pyenv: 简单的 Python 版本管理]

pyenv 允许你轻松切换多个 Python 版本。它简单、不干扰,并遵循 UNIX…

github.com](https://github.com/pyenv/pyenv?source=post_page-----29a2171d1d7c--------------------------------)

在我的研究中,我遇到了 Pyenv——一个 Python 版本管理工具。

类似于我们使用 condavenv 管理不同的软件包和环境,Pyenv 允许我们在同一台机器上拥有多个 Python 版本。除了实际的版本(例如,Python 3.8.1 和 Python 3.9.7),通过 Pyenv 我们还可以决定是否需要 x86 版本的 Python。

因此,在一台机器上,我们可以同时拥有 Python 3.9.7 的 x86 版本和 ARM64 版本。我们可以像处理系统本身的版本一样切换不同的 Python 版本。

我们仍然可以与 condavenv 一起使用它们。

那么,我们该如何开始使用 Pyenv 呢?

第 1 步:在系统上安装 xcode

xcode-select --install

第 2 步:安装 Homebrew

确保你已经安装了 brew。你可以在终端运行 brew help 来检查是否已安装。

如果你需要安装 brew,可以按照他们页面上的说明进行操作。

[## Homebrew]

macOS(或 Linux)的缺失包管理器。

brew.sh](https://brew.sh/?source=post_page-----29a2171d1d7c--------------------------------)

第 3 步:安装 Pyenv

我们可以通过 Homebrew 安装 Pyenv,方法如下:

brew install pyenv

第 4 步:更新 Shell 配置文件

我们需要更新我们的 shell 配置文件 PATH(例如,/.bash_profile、/.zshrc 等)以包含 Pyenv。

同时,为了使 Pyenv 默认加载,我们需要初始化它。

##### ~/.zprofile #####
eval "$(pyenv init --path)"

##### ~/.zshrc #####
if command -v pyenv 1>/dev/null 2>&1; then
    eval "$(pyenv init -)"
fi

第 5 步:安装 Python 版本

要列出所有可用的版本,我们可以使用:

pyenv install --list

要安装特定的 Python 版本,我们可以使用:

pyenv install 3.9.7

安装后,我们还可以将新版本设置为全局 Python 版本。

pyenv global 3.9.7

第 6 步:安装软件包

一切设置好后,我们现在可以像使用本机 Python 版本一样开始使用我们的新 Python 版本。我们可以创建不同的虚拟环境,并通过 pipsetup.py 安装不同的软件包,就像我们通常做的那样。

但是,x86 Python 环境怎么办?

我们可以使用 Rosetta 和 Pyenv 在 Apple Silicon 上设置 x86 版本的 Python。

Rosetta 是由 Apple 构建的一个工具,用于将 x86 架构转换为 ARM64。

我们可以使用以下命令安装 Rosetta:

softwareupdate --install-rosetta

安装完成后,我们可以设置终端使用 Rosetta 启动,这样终端将转换为 x86 模式。

为此,右键点击你的终端应用程序,点击 获取信息 并勾选 使用 Rosetta 打开

提示: 复制终端应用程序,这样你将拥有一个使用 Rosetta(即 x86)启动的终端,以及一个用于 ARM64 的终端。

你现在可以按照上述 x86 终端上的相同步骤来访问 x86 Python 环境。

结束语

在这篇文章中,我们将深入探讨一个简单但有效的解决方案,用于在 Apple Silicon 上使用 Pyenv 处理 x86 Python 版本。除了这个好处外,Pyenv 还提供了许多其他优势,包括在一台机器上管理多个 Python 版本。

你喜欢这篇文章吗?每月 $5,你可以成为会员,解锁对 Medium 的无限访问权限。这将直接支持我以及你在 Medium 上其他喜爱的作者。非常感谢!

## 使用我的推荐链接加入 Medium - David Farrugia

阅读 David Farrugia 的每个故事(以及 Medium 上其他成千上万的作家)。你的会员费将直接支持…

订阅

也许你还可以考虑订阅我的邮件列表,以便在我发布新内容时收到通知。这个服务是免费的 😃

## 每当 David Farrugia 发布新内容时,收到一封电子邮件。

每当 David Farrugia 发布新内容时,会收到一封电子邮件。通过注册,如果你还没有 Medium 账户,你将创建一个账户…

订阅

想要联系我吗?

我很想听听你对这个话题的看法,或者对 AI 和数据的任何想法。

如果你希望联系我,请发电子邮件到 davidfarrugia53@gmail.com

Linkedin

解决通用线性模型中的自相关问题的实际应用

原文:towardsdatascience.com/solving-autocorrelation-problems-in-general-linear-model-on-a-real-world-application-0bd3eeda20a1?source=collection_archive---------4-----------------------#2023-12-13

探索数据科学家最常遇到的噩梦之一

Rodrigo da Motta C. CarvalhoTowards Data Science Rodrigo da Motta C. Carvalho

·

关注 发表在 Towards Data Science · 8 min read · 2023 年 12 月 13 日

--

引言

线性回归中最大的一个问题是自相关残差。在这种情况下,本文重新审视了线性回归,深入探讨了 Cochrane–Orcutt 程序作为解决此问题的一种方法,并探索了在 fMRI 大脑激活分析中的实际应用。

图片由Jon Tyson拍摄,来源于Unsplash

回顾一般线性模型(GLM)

线性回归可能是任何数据科学家最重要的工具之一。然而,尤其在时间序列的背景下,常常会出现许多误解。因此,让我们花些时间重新审视这一概念。GLM 在时间序列分析中的主要目标是建模变量在时间点序列中的关系。其中Y是目标数据,X是特征数据,BA是待估计的系数,Ɛ是高斯误差。

GLM 的矩阵形式。图片由作者提供。

指数指的是数据的时间演变。以更紧凑的形式表示:

GLM 的矩阵形式。图片由作者提供。

作者提供。

参数的估计是通过普通最小二乘法(OLS)进行的,该方法假设观测值与模型预测值之间的误差或残差是独立且同分布的(i.i.d

这意味着残差必须是非自相关的,以确保系数的正确估计、模型的有效性和预测的准确性。

自相关

自相关指的是时间序列中观测值之间的相关性。我们可以理解为每个数据点与序列中滞后的数据点之间的关系。

自相关函数(ACF)用于检测自相关。这些方法测量数据点与其滞后值(t = 1,2,…,40)之间的相关性,揭示数据点是否与前面的或后面的值相关。ACF 图(图 1)展示了不同滞后下的相关系数,指示自相关的强度,以及阴影区域内的统计显著性。

图 1. ACF 图。图片由作者提供。

如果某些滞后的系数显著不同于零,这表明存在自相关。

残差中的自相关

残差中的自相关表明当前和过去的误差之间存在关系或依赖性。这种相关模式表明误差不是随机的,可能受到模型未考虑的因素的影响。例如,自相关可能导致参数估计偏差,尤其是在方差方面,影响对变量之间关系的理解。这会导致从模型中得出的无效推断,使对变量之间关系的结论产生误导。此外,它会导致预测效率低下,这意味着模型未能捕捉到正确的信息。

Cochrane–Orcutt 程序

Cochrane–Orcutt 程序是一种在经济计量学和其他领域著名的方法,用于通过线性模型处理时间序列中的自相关问题,尤其是在误差项的序列相关性方面[1,2]。我们已经知道这违背了普通最小二乘法(OLS)回归的假设,该假设认为误差(残差)是无相关的[1]。在本文稍后,我们将使用该程序去除自相关性,并检查系数的偏差。

Cochrane–Orcutt 程序如下:

  • 1. 初始 OLS 回归: 通过普通最小二乘法(OLS)进行初始回归分析,以估计模型参数。

初始回归方程。图片由作者提供。

  • 2. 残差计算: 计算初始回归的残差。

  • 3. 检测自相关性: 使用 ACF 图或如 Durbin-Watson 检验等测试检查残差是否存在自相关性。如果自相关性不显著,则无需继续该程序。

  • 4. 转换: 通过对依赖变量和自变量进行差分处理来转换估计模型,以去除自相关性。这里的想法是使残差更接近无相关性。

Cochrane–Orcutt 自回归项 AR(1)公式。图片由作者提供。

  • 5. 对转换后的模型进行回归: 使用转换后的模型进行新的回归分析,并计算新的残差。

  • 6. 检查自相关性: 再次测试新的残差是否存在自相关性。如果自相关性仍然存在,请返回第 4 步,进一步转换模型,直到残差不显示显著的自相关性。

最终模型估计:一旦残差不再显示显著的自相关性,使用从 Cochrane-Orcutt 程序得出的最终模型和系数进行推断和得出结论!

实际应用:功能性磁共振成像(fMRI)分析

fMRI 简要介绍

功能性磁共振成像(fMRI)是一种神经成像技术,通过检测血流变化来测量和映射大脑活动。它依赖于神经活动与血流和氧合增加的原理。在 fMRI 中,当大脑区域变得活跃时,它会触发血流动力学反应,导致血氧水平依赖(BOLD)信号的变化。fMRI 数据通常由表示不同时间点大脑激活的 3D 图像组成,因此每个体积(体素)的大脑都有自己的时间序列(图 2)。

图 2. 体素的时间序列(BOLD 信号)表示。图片由作者提供。

一般线性模型(GLM)

GLM 假设测量的 fMRI 信号是不同因素(特征)的线性组合,如任务信息与被称为血流动力学响应函数(HRF)的神经活动预期反应。为了简化,我们将忽略 HRF 的性质,仅假设它是一个重要的特征。

为了理解任务对结果 BOLD 信号y (因变量)的影响,我们将使用 GLM。这意味着通过与任务信息相关的统计显著系数来检查影响。因此,X1X2 (自变量) 是参与者通过数据收集执行的任务的信息,与 HRF 卷积(见图 3)。

GLM 的矩阵公式。图片由作者提供。

在实际数据上的应用

为了检查这个实际应用,我们将使用由 João Sato 教授在 ABC 联邦大学收集的数据,这些数据可在GitHub上获取。自变量fmri_data包含来自一个体素(一个时间序列)的数据,但我们可以对大脑中的每个体素进行类似操作。包含任务信息的因变量是congincong。这些变量的解释超出了本文的范围。

#Reading data
fmri_img = nib.load('/Users/rodrigo/Medium/GLM_Orcutt/Stroop.nii')
cong = np.loadtxt('/Users/rodrigo/Medium/GLM_Orcutt/congruent.txt')
incong = np.loadtxt('/Users/rodrigo/Medium/GLM_Orcutt/incongruent.txt')

#Get the series from each voxel
fmri_data = fmri_img.get_fdata()

#HRF function
HRF = glover(.5)

#Convolution of task data with HRF
conv_cong = np.convolve(cong.ravel(), HRF.ravel(), mode='same')
conv_incong = np.convolve(incong.ravel(), HRF.ravel(), mode='same')

可视化任务信息变量(特征)。

图 3。任务信息与血流动力学响应函数(特征)混合。图片由作者提供。

拟合 GLM

使用普通最小二乘法拟合模型并估计模型参数,我们得到

import statsmodels.api as sm

#Selecting one voxel (time series)
y = fmri_data[20,30,30]
x = np.array([conv_incong, conv_cong]).T

#add constant to predictor variables
x = sm.add_constant(x)

#fit linear regression model
model = sm.OLS(y,x).fit()

#view model summary
print(model.summary())
params = model.params

BOLD 信号与回归。图片由作者提供。

GLM 系数。图片由作者提供。

可以看出系数 X1 在统计上显著,因为 P > |t| 小于 0.05。这可能意味着任务确实对 BOLD 信号有影响。 但在使用这些参数进行推断之前,必须检查残差,即y减去预测,在任何滞后中是否没有自相关。否则,我们的估计是有偏的。

检查残差自相关

正如已经讨论过的,ACF 图是一种检查序列中自相关的好方法。

ACF 图。图片由作者提供。

从 ACF 图中可以检测到滞后 1 的高自相关。因此,这个线性模型是有偏的,重要的是要解决这个问题。

Cochrane-Orcutt 解决残差自相关问题

Cochrane-Orcutt 程序在 fMRI 数据分析中广泛用于解决这类问题[2]。在这个特定的案例中,残差的滞后 1 自相关显著,因此我们可以使用 Cochrane–Orcutt 公式来处理自回归项 AR(1)。

Cochrane–Orcutt 公式用于自回归项 AR(1)。图像由作者提供。

# LAG 0
yt = y[2:180]
# LAG 1
yt1 = y[1:179]

# calculate correlation coef. for lag 1
rho= np.corrcoef(yt,yt1)[0,1]

# Cochrane-Orcutt equation
Y2= yt - rho*yt1
X2 = x[2:180,1:] - rho*x[1:179,1:]

拟合变换后的模型

在 Cochrane-Orcutt 校正后再次拟合模型。

import statsmodels.api as sm

#add constant to predictor variables
X2 = sm.add_constant(X2)

#fit linear regression model
model = sm.OLS(Y2,X2).fit()

#view model summary
print(model.summary())
params = model.params

BOLD 信号和变换后的 GLM。图像由作者提供。

GLM 系数。图像由作者提供。

现在系数 X1 已不再具有统计显著性,排除了任务对 BOLD 信号的影响的假设。参数的标准误差估计显著变化,这表明残差中的自相关对估计具有很高的影响。

再次检查自相关

这很有意义,因为可以证明当存在自相关时方差总是有偏的 [1]。

ACF 图。图像由作者提供。

现在残差的自相关已被去除,估计不再有偏。如果我们忽略了残差中的自相关,我们可能会认为系数是显著的。然而,在去除自相关之后,结果表明该参数并不显著,从而避免了错误推断任务确实与信号相关。

结论

广义线性模型中的残差自相关可能导致估计偏差、预测效率低下和推断无效。将 Cochrane–Orcutt 程序应用于实际 fMRI 数据,展示了其在去除残差自相关和避免虚假结论中的有效性,确保了模型参数的可靠性和从分析中得出的结论的准确性。

备注

Cochrane-Orcutt 只是解决残差自相关的一种方法。然而,还有其他方法可以解决这个问题,例如 Hildreth-Lu 方法和差分方法 [1]。

致谢

本项目受到 João Ricardo Sato 教授的启发。

本文的笔记本可以在 这里获取

参考文献

[1] 应用回归建模, Iain Pardoe. Wileyl. 2023.online.stat.psu.edu/stat462/node/189/

[2] Sato JR, Takahashi DY, Cardoso EF, Martin Mda G, Amaro Júnior E, Morettin PA. 功能连接识别中的干预模型应用于 FMRI. Int J Biomed Imaging. 2006;2006:27483. doi:10.1155/IJBI/2006/27483

使用 PyTorch Profiler 和 TensorBoard 解决数据输入管道瓶颈

原文:towardsdatascience.com/solving-bottlenecks-on-the-data-input-pipeline-with-pytorch-profiler-and-tensorboard-5dced134dbe9?source=collection_archive---------2-----------------------#2023-08-26

PyTorch 模型性能分析与优化——第四部分

Chaim RandTowards Data Science Chaim Rand

·

关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 8 月 26 日

--

图片来源:Alexander GreyUnsplash

这是我们关于 GPU 基于 PyTorch 工作负载的性能分析和优化系列文章中的第四篇。我们将在这篇文章中重点关注训练数据输入管道。在典型的训练应用中,主机的 CPU 加载、预处理和整理数据,然后将其送入 GPU 进行训练。当主机无法跟上 GPU 的速度时,输入管道中会出现瓶颈。这会导致 GPU —— 训练设置中最昂贵的资源——在等待来自过度负荷主机的数据输入时闲置。在之前的文章中(例如,这里),我们详细讨论了输入管道瓶颈,并回顾了应对这些瓶颈的不同方法,例如:

  1. 选择一个 CPU 到 GPU 计算比率更适合你的工作负载的训练实例(例如,参见我们 之前的帖子 关于选择最适合你的 ML 工作负载的实例类型的提示),

  2. 通过将一些 CPU 预处理活动转移到 GPU 来改善 CPU 和 GPU 之间的工作负载平衡,并

  3. 将部分 CPU 计算任务转移到辅助 CPU 工作设备(例如,参见 这里)。

当然,解决数据输入管道中的性能瓶颈的第一步是识别并理解它。在这篇文章中,我们将演示如何使用 PyTorch Profiler 及其相关的 TensorBoard 插件 来实现这一点。

正如我们之前的帖子中所述,我们将定义一个玩具 PyTorch 模型,然后迭代地分析其性能,识别瓶颈,并尝试解决它们。我们将在一个 Amazon EC2 g5.2xlarge 实例上运行我们的实验(该实例包含 NVIDIA A10G GPU 和 8 个 vCPUs),并使用官方的 AWS PyTorch 2.0 Docker 镜像。请记住,我们描述的某些行为可能因 PyTorch 版本不同而有所变化。

感谢 Yitzhak Levi 对这篇文章的贡献。

玩具模型

在接下来的部分中,我们介绍了我们将用于演示的玩具示例。我们首先定义了一个简单的图像分类模型。模型的输入是一个256x256的 YUV 图像批次,输出是其相关的语义类别预测批次。

from math import log2
import torch
import torch.nn as nn
import torch.nn.functional as F

img_size = 256
num_classes = 10
hidden_size = 30

# toy CNN classification model
class Net(nn.Module):
    def __init__(self, img_size=img_size, num_classes=num_classes):
        super().__init__()
        self.conv_in = nn.Conv2d(3, hidden_size, 3, padding='same')
        num_hidden = int(log2(img_size))
        hidden = []
        for i in range(num_hidden):
            hidden.append(nn.Conv2d(hidden_size, hidden_size, 3, padding='same'))
            hidden.append(nn.ReLU())
            hidden.append(nn.MaxPool2d(2))
        self.hidden = nn.Sequential(*hidden)
        self.conv_out = nn.Conv2d(hidden_size, num_classes, 3, padding='same')

    def forward(self, x):
        x = F.relu(self.conv_in(x))
        x = self.hidden(x)
        x = self.conv_out(x)
        x = torch.flatten(x, 1)
        return x

下面的代码块包含我们的数据集定义。我们的数据集包含一万张 JPEG 图像的文件路径及其相关(随机生成的)语义标签。为了简化演示,我们将假设所有 JPEG 文件路径指向相同的图像——本文顶部的多彩“瓶颈”图片。

import numpy as np
from PIL import Image
from torchvision.datasets.vision import VisionDataset
input_img_size = [533, 800]
class FakeDataset(VisionDataset):
    def __init__(self, transform):
        super().__init__(root=None, transform=transform)
        size = 10000
        self.img_files = [f'0.jpg' for i in range(size)]
        self.targets = np.random.randint(low=0,high=num_classes,
                                         size=(size),dtype=np.uint8).tolist()

    def __getitem__(self, index):
        img_file, target = self.img_files[index], self.targets[index]
        with torch.profiler.record_function('PIL open'):
            img = Image.open(img_file)
        if self.transform is not None:
            img = self.transform(img)
        return img, target

    def __len__(self):
        return len(self.img_files)

请注意,我们已经使用 torch.profiler.record_function 上下文管理器包装了文件读取器。

我们的输入数据管道包括对图像进行以下转换:

  1. PILToTensor 将 PIL 图像转换为 PyTorch Tensor。

  2. RandomCrop 在图像的随机偏移处返回一个 256x256 的裁剪区域。

  3. RandomMask 是一种自定义转换,创建一个随机的 256x256 布尔掩码并将其应用于图像。该转换包括对掩码的四邻域膨胀操作。

  4. ConvertColor 是一种自定义转换,将图像格式从 RGB 转换为 YUV。

  5. Scale 是一种自定义转换,将像素缩放到 [0,1] 范围内。

class RandomMask(torch.nn.Module):
    def __init__(self, ratio=0.25):
        super().__init__()
        self.ratio=ratio

    def dilate_mask(self, mask):
        # perform 4 neighbor dilation on mask
        with torch.profiler.record_function('dilation'):
            from scipy.signal import convolve2d
            dilated = convolve2d(mask, [[0, 1, 0],
                                     [1, 1, 1],
                                     [0, 1, 0]], mode='same').astype(bool)
        return dilated

    def forward(self, img):
        with torch.profiler.record_function('random'):
            mask = np.random.uniform(size=(img_size, img_size)) < self.ratio
        dilated_mask = torch.unsqueeze(torch.tensor(self.dilate_mask(mask)),0)
        dilated_mask = dilated_mask.expand(3,-1,-1)
        img[dilated_mask] = 0.
        return img

    def __repr__(self):
        return f"{self.__class__.__name__}(ratio={self.ratio})"

class ConvertColor(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.A=torch.tensor(
            [[0.299, 0.587, 0.114],
             [-0.16874, -0.33126, 0.5],
             [0.5, -0.41869, -0.08131]]
        )
        self.b=torch.tensor([0.,128.,128.])

    def forward(self, img):
        img = img.to(dtype=torch.get_default_dtype())
        img = torch.matmul(self.A,img.view([3,-1])).view(img.shape)
        img = img + self.b[:,None,None]
        return img

    def __repr__(self):
        return f"{self.__class__.__name__}()"

class Scale(object):
    def __call__(self, img):
        return img.to(dtype=torch.get_default_dtype()).div(255)

    def __repr__(self):
        return f"{self.__class__.__name__}()"

我们使用 Compose 类链接这些转换,我们稍微修改了它,以在每个转换调用周围包含一个 torch.profiler.record_function 上下文管理器。

import torchvision.transforms as T
class CustomCompose(T.Compose):
    def __call__(self, img):
        for t in self.transforms:
            with torch.profiler.record_function(t.__class__.__name__):
                img = t(img)
        return img

transform = CustomCompose(
    [T.PILToTensor(),
     T.RandomCrop(img_size),
     RandomMask(),
     ConvertColor(),
     Scale()])

在下面的代码块中,我们定义了数据集和数据加载器。我们将 DataLoader 配置为使用 自定义 collate 函数,在其中我们使用 torch.profiler.record_function 上下文管理器包装 默认 collate 函数

train_set = FakeDataset(transform=transform)

def custom_collate(batch):
    from torch.utils.data._utils.collate import default_collate
    with torch.profiler.record_function('collate'):
        batch = default_collate(batch)
    image, label = batch
    return image, label

train_loader = torch.utils.data.DataLoader(train_set, batch_size=256,
                                           collate_fn=custom_collate,
                                           num_workers=4, pin_memory=True)

最后,我们定义了模型、损失函数、优化器和训练循环,并用 profiler 上下文管理器 包装。

from statistics import mean, variance
from time import time

device = torch.device("cuda:0")
model = Net().cuda(device)
criterion = nn.CrossEntropyLoss().cuda(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
model.train()

t0 = time()
times = []

with torch.profiler.profile(
    schedule=torch.profiler.schedule(wait=10, warmup=2, active=10, repeat=1),
    on_trace_ready=torch.profiler.tensorboard_trace_handler('/tmp/prof'),
    record_shapes=True,
    profile_memory=True,
    with_stack=True
) as prof:
    for step, data in enumerate(train_loader):
        with torch.profiler.record_function('h2d copy'):
            inputs, labels = data[0].to(device=device, non_blocking=True), \
                             data[1].to(device=device, non_blocking=True)
        if step >= 40:
            break
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        optimizer.zero_grad(set_to_none=True)
        loss.backward()
        optimizer.step()
        prof.step()
        times.append(time()-t0)
        t0 = time()

print(f'average time: {mean(times[1:])}, variance: {variance(times[1:])}')

在接下来的部分中,我们将使用 PyTorch Profiler 及其相关的 TensorBoard 插件 来评估我们模型的性能。我们的重点将是分析器报告的 Trace View。有关如何使用报告的其他部分,请参阅我们系列中的 第一篇文章。

初步性能结果

我们定义的脚本报告的平均步长时间为 1.3 秒,平均 GPU 利用率非常低,为 18.21%。在下面的图像中,我们捕捉了在 TensorBoard 插件 跟踪视图 中显示的性能结果。

基线模型的跟踪视图(作者拍摄)

我们可以看到每隔四步训练就会出现一个较长的 (~5.5 秒) 数据加载期间,在此期间 GPU 完全空闲。这种现象发生在每隔四步的原因与我们选择的 DataLoader 工作线程数量—四个—直接相关。每隔四步,我们发现所有工作线程都在忙于生成下一批次的样本,而 GPU 则在等待。这清楚地表明数据输入管道中存在瓶颈。问题是我们该如何分析它?使问题复杂化的是,我们插入代码中的许多 record_function 标记在性能分析轨迹中找不到。

在 DataLoader 中使用多个工作线程对优化性能至关重要。不幸的是,这也使得性能分析过程变得更加困难。虽然存在支持多进程分析的性能分析工具(例如,查看 VizTracer),但我们在本文中采用的方法是以单进程模式(即,零 DataLoader 工作线程)运行、分析和优化我们的模型,然后将优化应用到多工作线程模式。诚然,优化一个独立函数的速度并不能保证多个(并行)调用相同函数也会有同样的效果。然而,正如我们在本文中将看到的,这一策略将帮助我们识别并解决一些我们无法通过其他方式识别的核心问题,并且,至少在这里讨论的问题方面,我们发现这两种模式的性能影响之间有很强的关联。但在应用这一策略之前,让我们调整工作线程的数量选择。

优化 1:调整多进程策略

确定多进程/多线程应用程序(例如我们的应用程序)中的最佳线程或进程数量可能会很棘手。一方面,如果我们选择的数量太少,可能会导致 CPU 资源未被充分利用。另一方面,如果数量设置得过高,我们就有可能面临thrashing的风险,这是一个不希望出现的情况,操作系统花费大部分时间来管理多个线程/进程,而不是运行我们的代码。在 PyTorch 训练工作负载的情况下,建议尝试不同的 DataLoader num_workers设置。一个好的起点是根据主机上的 CPU 数量来设置这个数字(例如,num_workers:=num_cpus/num_gpus)。在我们的案例中,Amazon EC2 g5.2xlarge有八个 vCPUs,实际上,将 DataLoader 的工作线程数量增加到八个会导致稍微更好的平均步骤时间为 1.17 秒(提升 11%)。

重要的是,要留意其他不太明显的配置设置,这些设置可能会影响数据输入管道使用的线程或进程数量。例如,opencv-python,这是一个常用于计算机视觉工作负载中的图像预处理的库,其中包括用于控制线程数量的cv2.setNumThreads(int)函数。

在下面的图像中,我们捕捉到在将num_workers设置为时运行脚本的Trace View的一部分。

单进程模式下基线模型的跟踪视图(作者拍摄)

以这种方式运行脚本使我们能够看到我们设置的record_function标签,并识别出RandomMask变换,或者更具体地说,我们的dilation函数,作为检索每个单独样本时最耗时的操作。

优化 2:优化膨胀函数

我们当前的膨胀函数实现使用 2D 卷积,通常使用矩阵乘法实现,并且在 CPU 上运行速度不是特别快。一个选项是将膨胀操作在 GPU 上运行(如这篇文章中所述)。然而,主机与设备之间的事务开销可能会超过这种解决方案带来的潜在性能提升,更不用说我们不愿增加 GPU 的负担。

在下面的代码块中,我们提出了一种更为 CPU 友好的膨胀函数实现,它使用布尔操作而不是卷积:

 def dilate_mask(self, mask):
        # perform 4 neighbor dilation on mask
        with torch.profiler.record_function('dilation'):
            padded = np.pad(mask, [(1,1),(1,1)])
            dilated = padded[0:-2,1:-1] | padded[1:-1,1:-1] | padded[2:,1:-1] | padded[1:-1,0:-2]| padded[1:-1,2:]
        return dilated

经过这种修改后,我们的步骤时间降至 0.78 秒,相当于额外提高了 50%。更新后的单进程Trace-View如下所示:

单进程模式下膨胀优化后的跟踪视图(作者拍摄)

我们可以看到,膨胀操作已经显著缩小,现在最耗时的操作是PILToTensor变换。

更详细地查看PILToTensor函数(见这里)可以发现三个潜在的操作:

  1. 由于Image.open的延迟加载特性,PIL 图像在此处被加载。

  2. PIL 图像被转换为 numpy 数组。

  3. numpy 数组被转换为 PyTorch Tensor。

尽管图像加载占用了大部分时间,但我们注意到对全尺寸图像进行随后的操作并立即裁剪的极端浪费。这引出了我们下一步的优化。

优化 3:重排变换

幸运的是,RandomCrop变换可以直接应用于 PIL 图像,使我们能够将图像尺寸缩减作为管道中的第一个操作:

transform = CustomCompose(
    [T.RandomCrop(img_size),
     T.PILToTensor(),
     RandomMask(),
     ConvertColor(),
     Scale()])

在这一优化之后,我们的步骤时间降至 0.72 秒,额外的 8%优化。下图中的Trace View捕获显示随机裁剪变换现在是主要操作:

单进程模式下变换重排后的跟踪视图(作者拍摄)

实际上,与之前一样,瓶颈实际上是 PIL 图像(延迟)加载,而不是随机裁剪。

理想情况下,我们可以通过将读取操作限制为仅对感兴趣的裁剪区域进行优化。不幸的是,截止到本文撰写时,torchvision不支持此选项。在未来的帖子中,我们将展示如何通过实现我们自己的custom decode_and_crop PyTorch 操作符来克服这一不足。

优化 4:应用批处理变换

在我们当前的实现中,每个图像变换都是对每个图像单独应用的。然而,当一次性对整个批量应用某些变换时,它们可能会运行得更优。下面的代码块中,我们修改了我们的管道,以便在我们的custom collate function中对图像批量应用 ColorTransformation 和 Scale 变换:

def batch_transform(img):
    img = img.to(dtype=torch.get_default_dtype())
    A = torch.tensor(
        [[0.299, 0.587, 0.114],
         [-0.16874, -0.33126, 0.5],
         [0.5, -0.41869, -0.08131]]
    )
    b = torch.tensor([0., 128., 128.])

    A = torch.broadcast_to(A, ([img.shape[0],3,3]))
    t_img = torch.bmm(A,img.view(img.shape[0],3,-1))
    t_img = t_img + b[None,:, None]
    return t_img.view(img.shape)/255

def custom_collate(batch):
    from torch.utils.data._utils.collate import default_collate
    with torch.profiler.record_function('collate'):
        batch = default_collate(batch)

    image, label = batch
    with torch.profiler.record_function('batch_transform'):
        image = batch_transform(image)
    return image, label

这个变化的结果实际上是步骤时间略微增加,达到 0.75 秒。虽然对于我们的玩具模型没有帮助,但将某些操作应用为批量变换而不是每个样本变换的能力,具有优化特定工作负载的潜力。

结果

我们在这篇文章中应用的连续优化使得运行时性能提高了 80%。然而,尽管改进的效果不那么显著,输入管道仍存在瓶颈,而且 GPU 的利用率依然较低(约 30%)。请参考我们之前的文章(例如,这里)以获取解决此类问题的额外方法。

总结

在这篇文章中,我们关注了训练数据输入管道中的性能问题。正如我们在本系列的之前文章中所做的,我们选择了 PyTorch Profiler 及其关联的 TensorBoard 插件作为我们首选的工具,并展示了它们在加速训练速度方面的使用。特别是,我们展示了如何通过使用零工作线程运行 DataLoader,来提高识别、分析和优化数据输入管道瓶颈的能力。

正如我们在之前的文章中强调的,成功优化的路径将根据训练项目的细节,包括模型架构和训练环境,存在很大差异。实际上,实现你的目标可能比我们这里展示的例子更为困难。我们描述的一些技术可能对你的性能影响不大,甚至可能使其变得更差。我们还注意到,我们选择的具体优化及其应用顺序有些任意。我们强烈鼓励你根据项目的具体细节开发自己的工具和技术来实现优化目标。

使用 Python 解决地理旅行推销员问题

原文:towardsdatascience.com/solving-geographic-travelling-salesman-problems-using-python-e57284b14cd7?source=collection_archive---------1-----------------------#2023-07-12

使用 pyconcorde 寻找现实世界路由问题的最佳解决方案

Mike JonesTowards Data Science Mike Jones

·

关注 发表在 Towards Data Science ·12 分钟阅读·2023 年 7 月 12 日

--

一条最佳的汽车驾驶路线,连接 79 个英国城市。图像由作者提供。地图数据来自 OpenStreetMap

著名的旅行推销员问题(TSP)是关于在一组节点(城市)之间找到最佳路线并返回起点。这听起来很简单,但对于大量节点而言,通过蛮力求解是不可能的,因为n个城市的可能排序数量为n!。这意味着,即使是 30 个城市,你需要检查的旅行数量也是 265,252,859,812,191,058,636,308,480,000,000。即使是强大的计算机也无法通过蛮力解决大型 TSP 问题。

随机生成的 10 节点 TSP:需要检查 3,628,800 条可能的路线。图片由作者提供。

幸运的是,已经开发出一些算法,大大减少了解决大型 TSP 所需的计算量。几十年前开发的Concorde软件在学术界得到广泛使用。虽然作为独立软件使用时相当技术性,并且仅面向专业人员,但pyconcorde作为 Concorde 的 Python 包装器被开发出来。Concorde 中使用的算法的解释超出了本文的范围。然而,我们将深入探讨在 Python 中重现这些问题及其解决方案所需的代码。

现实世界的地理 TSP

如何解决实际的地理旅行推销员问题?现实世界的点不像上图那样通过简单的 2D 线连接。相反,地理特征通过各种可能的路线连接,而这些路线会根据是步行、骑自行车还是开车而变化。

数据科学家或软件工程师为何要解决实际的 TSP?以下是一些用例示例:

  1. 一家公司雇用快递员,需要一种计算城市中最佳路线的方法,以最小化每位司机的道路时间。

  2. 旅游运营商需要在有限的时间内找到连接一组目的地的最短路线。

  3. 废物处理公司或地方当局需要分配资源,以确保尽可能高效地安排取货。

为了解决实际的 TSP,可以使用routingpy库来查找路线、距离(以米为单位)和地理点之间的持续时间(以秒为单位),格式为[经度, 纬度]对。本文将描述可用于此类问题的方法。

编码讲解

这里概述了使用 Python 解决地理 TSP 的指南。问题解决过程的基本结构如下:

  1. 获取一个包含n个坐标的列表,格式为[经度, 纬度]对。

  2. 使用路由服务获取一个 (n x n) 矩阵,表示这些坐标之间的现实世界持续时间,适用于相应的配置文件(步行、骑行、驾车、驾驶重型货车等)。此矩阵将是非对称的(从 A 到 B 的驾驶时间并非 B 到 A 的完全反向)。

  3. 将 (n x n) 矩阵转换为对称矩阵 (2n x 2n)。

  4. 将此矩阵输入 Concorde 求解器以找到坐标的最佳顺序。

  5. 使用路由服务创建现实世界的路线。

  6. 在地图上可视化结果。

  7. 可选:创建最终路线的 GPX 文件。

每一步将详细讲解。

第 1 步:获取坐标

对于我们的示例,我们将考虑在英国的 79 个城市之间驾车旅行的问题。下图显示了蓝色的英国城市地图。数据科学家可以通过多种方式找到坐标。如果需要,可以使用 Google Maps 或 Google Earth 手动查找坐标。

英国的 79 个城市。图像由作者提供。地图数据来自 OpenStreetMap

本示例中使用的代码结构和数据也可以在 此 GitHub 仓库中找到。

这里是一个包含城市坐标的 CSV 文件(gb_cities.csv 在仓库中),以及使用 pandas 导入它所需的代码。

Place Name,Latitude,Longitude
Aberdeen,57.149651,-2.099075
Ayr,55.458565,-4.629179
Basildon,51.572376,0.470009
Bath,51.380001,-2.36
Bedford,52.136436,-0.460739
...
import pandas as pd
df = pd.read_csv('gb_cities.csv')
coordinates = df[['Longitude', 'Latitude']].values
names = df['Place Name'].values

第 2 步:使用路由服务获取持续时间矩阵

routingpy 库提供了多种路由服务。来自 Graphhopper 的 API 包含一个免费层,允许有限的使用。通过 routingpy 还可以使用其他路由器,详细信息见 文档

import routingpy as rp
import numpy as np

api_key = # get a free key at https://www.graphhopper.com/
api = rp.Graphhopper(api_key=api_key)
matrix = api.matrix(locations=coordinates, profile='car')
durations = np.matrix(matrix.durations)
print(durations)

这是 durations,一个 79 x 79 的矩阵,表示坐标之间的驾驶时间(以秒为单位):

matrix([[    0, 10902, 30375, ..., 23380, 25233, 19845],
        [10901,     0, 23625, ..., 16458, 18312, 13095],
        [30329, 23543,     0, ...,  8835,  9441, 12260],
        ...,
        [23397, 16446,  9007, ...,     0,  2789,  7924],
        [25275, 18324,  9654, ...,  2857,     0,  9625],
        [19857, 13071, 12340, ...,  8002,  9632,     0]])

城市之间的驾驶时间可以通过以下方法确定:

  1. 每一行和每一列对应一个城市:阿伯丁是第一行和第一列,艾尔是第二行,巴斯尔顿是第三行,依此类推。

  2. 要找到阿伯丁和艾尔之间的时间,请查看第 1 行第 2 列:10,902 秒。反向时间(艾尔到阿伯丁)为 10,901 秒。

  3. 一般来说,从第 i 个城市到第 j 个城市的时间位于第 i 行和第 j 列的交点处。

注意矩阵中对角线上的零,因为每个点与自身的距离或持续时间为零。此外,矩阵并非完全对称:城市之间的驾驶时间在两个方向上不太可能完全相同,原因是道路布局和交通热点不同。不过,它们大致相似,这也是预期中的情况。

第 3 步:将非对称矩阵转换为对称矩阵

在使用该矩阵生成 pyconcorde 中的最佳排序之前,我们需要使矩阵对称。有关将非对称 TSP 转化为对称 TSP 的方法,请参见Jonker 和 Volgenant (1983):将非对称问题转化为对称旅行商问题,《运筹学快报》,2(4),161–163。以下是这种转换的理论。如果需要,可以跳过本节(滚动到标题为将地理非对称 TSP 转化为对称 TSP的部分)**。

Jonker/Volgenant 非对称到对称的转换

以下是具有 3 个节点的非对称 TSP 的可视化及其距离矩阵。

具有 3 个节点的非对称 TSP。图片由作者提供。

matrix([[0, 5, 2],
        [7, 0, 4],
        [3, 4, 0]])

下面是将其转化为对称 TSP 的方法草图:

  1. 创建新的幽灵节点,A'、B'和 C'。将 A 连接到 A',B 连接到 B',C 连接到 C',距离为零。

  2. 以如下方式连接节点并赋予权重:

    A 到 B 现在由 A'到 B 表示;B 到 A 现在由 B'到 A 表示。

    B 到 C 现在由 B'到 C 表示;C 到 B 现在由 C'到 B 表示。

    C 到 A 现在由 C'到 A 表示;A 到 C 现在由 A'到 C 表示。

  3. 将所有其他边的权重设为无限,以便任何算法不会尝试在它们之间进行旅行。由于在使用 pyconcorde 时这将不切实际,因此将所有其他权重设为远高于我们已有的最高权重。在这种情况下,我们将其设置为 99。

等效的对称 TSP,具有(3 x 2)节点。图片由作者提供。

这是生成的距离矩阵。矩阵中节点的顺序是:A、B、C、A'、B'、C'。

matrix([[ 0, 99, 99,  0,  7,  3],
        [99,  0, 99,  5,  0,  4],
        [99, 99,  0,  2,  4,  0],
        [ 0,  5,  2,  0, 99, 99],
        [ 7,  0,  4, 99,  0, 99],
        [ 3,  4,  0, 99, 99,  0]])

再次注意对角线是零,这符合预期,并且矩阵现在是对称的。原始矩阵位于新矩阵的左下角,其转置矩阵位于右上角。与此同时,左上角和右下角部分包含节点之间非常高的权重。

A、B 和 C(左上角)不再彼此连接(严格来说,它们是连接的,但具有非常高的权重而不是无限权重,出于实际考虑)。这意味着任何算法都不会尝试在这些节点之间寻找路径。同样,A'、B'和 C'(右下角)彼此也没有连接。相反,原始非对称网络的方向性在这里由原始节点 A、B 和 C 上的权重以及它们的幽灵节点 A'、B'和 C'表示。

原始非对称问题的解与新的对称 TSP 之间存在一一对应关系:

  • A — B — C — A 对应于 A — A' — B — B' — C — C' — A

  • A — C — B — A 对应于 A — A' — C — C' — B — B' — A

在每种情况下,幽灵节点 A'、B'和 C'与原始节点 A、B 和 C 交替出现,每个原始节点都与其“伙伴”幽灵节点相邻(A 与 A'相邻,依此类推)。

将地理非对称 TSP 转化为对称 TSP

回到我们的实际示例。我们可以创建一个函数,将不对称 TSP 矩阵转换为对称矩阵:

def symmetricize(m, high_int=None):

    # if high_int not provided, make it equal to 10 times the max value:
    if high_int is None:
        high_int = round(10*m.max())

    m_bar = m.copy()
    np.fill_diagonal(m_bar, 0)
    u = np.matrix(np.ones(m.shape) * high_int)
    np.fill_diagonal(u, 0)
    m_symm_top = np.concatenate((u, np.transpose(m_bar)), axis=1)
    m_symm_bottom = np.concatenate((m_bar, u), axis=1)
    m_symm = np.concatenate((m_symm_top, m_symm_bottom), axis=0)

    return m_symm.astype(int) # Concorde requires integer weights

symmetricize(durations) 返回:

matrix([[     0, 461120, 461120, ...,  23397,  25275,  19857],
        [461120,      0, 461120, ...,  16446,  18324,  13071],
        [461120, 461120,      0, ...,   9007,   9654,  12340],
        ...,
        [ 23397,  16446,   9007, ...,      0, 461120, 461120],
        [ 25275,  18324,   9654, ..., 461120,      0, 461120],
        [ 19857,  13071,  12340, ..., 461120, 461120,      0]])

这个 158 x 158 的矩阵在左下角包含 durations 的副本,在右上角包含转置副本。高达 461,120 的值(是 durations 中最大值的 10 倍)意味着在实际应用中,具有这种时长的节点是不连接的。

这个矩阵最终可以输入到 pyconcorde 中,以计算最佳路径。

第 4 步:使用 Concorde 求解器

安装 pyconcorde

运行以下命令来安装 pyconcorde(目前在 Linux 或 Mac OS 上可用,但 Windows 上不可用):

virtualenv venv                                  # create virtual environment
source venv/bin/activate                         # activate it
git clone https://github.com/jvkersch/pyconcorde # clone git repo
cd pyconcorde                                    # change directory
pip install -e .                                 # install pyconcorde

在 Python 中解决 TSP

现在我们可以在 Python 脚本中从 concorde 导入。

from concorde.problem import Problem
from concorde.concorde import Concorde

def solve_concorde(matrix):
    problem = Problem.from_matrix(matrix)
    solver = Concorde()
    solution = solver.solve(problem)
    print(f'Optimal tour: {solution.tour}')
    return solution

我们的对称时长矩阵可以输入到 solve_concorde() 中。

durations_symm = symmetricize(durations)
solution = solve_concorde(durations_symm)

以下是打印输出:

Optimal tour: [0, 79, 22, 101, 25, 104, 48, 127, 68, 147, 23, 102, 58, 137, 7, 86, 39, 118, 73, 152, 78, 157, 36, 115, 42, 121, 62, 141, 16, 95, 20, 99, 51, 130, 40, 119, 19, 98, 59, 138, 50, 129, 54, 133, 27, 106, 10, 89, 4, 83, 66, 145, 33, 112, 14, 93, 2, 81, 45, 124, 32, 111, 11, 90, 29, 108, 34, 113, 24, 103, 8, 87, 17, 96, 56, 135, 64, 143, 61, 140, 75, 154, 52, 131, 71, 150, 18, 97, 3, 82, 9, 88, 74, 153, 55, 134, 72, 151, 28, 107, 12, 91, 70, 149, 65, 144, 35, 114, 31, 110, 77, 156, 63, 142, 41, 120, 69, 148, 6, 85, 76, 155, 67, 146, 15, 94, 44, 123, 47, 126, 60, 139, 57, 136, 38, 117, 13, 92, 5, 84, 43, 122, 49, 128, 46, 125, 21, 100, 1, 80, 30, 109, 53, 132, 37, 116, 26, 105]

这个解决方案展示了最佳旅游路线中节点的排序。请注意,如上所述,该解决方案包含原始节点(编号为 0 到 78)和它们的虚拟节点(79 到 157)交替出现:

  • 0 与 79 配对,

  • 22 与 101 配对,

  • 25 与 104,以此类推……

这表明解决方案已正确工作。

第 5 步:创建现实世界的路线

下一步是选择解决方案中的交替元素(对应于原始 79 个城市的节点),然后相应地排序坐标。

# pick alternate elements: these correspond to the originals
tour = solution.tour[::2]

# order the original coordinates and names
coords_ordered = [coordinates[i].tolist() for i in tour]
names_ordered = [names[i] for i in tour]

这是 names_ordered 中的前几个城市名称,(最佳旅游路线中城市的真实排序):

['Aberdeen',
 'Dundee',
 'Edinburgh',
 'Newcastle Upon Tyne',
 'Sunderland',
 'Durham',
 ...]

现在我们重新加入第一个城市,以形成一个完整的循环路线,并最终使用 Graphhopper 方向 API 获取最终路线。

# add back in the first for a complete loop
coords_ordered_return = coords_ordered + [coords_ordered[0]]

# obtain complete driving directions for the ordered loop
directions = api.directions(locations=coords_ordered_return, profile='car')

第 6 步:在地图上可视化

在地图上查看最终路线将使我们对结果充满信心,并允许我们在实际设置中使用解决方案。以下代码将显示一个可以保存为 HTML 的 folium 地图。

import folium
def generate_map(coordinates, names, directions):

    # folium needs lat, long
    coordinates = [(y, x) for (x, y) in coordinates]
    route_points = [(y, x) for (x, y) in directions.geometry]
    lat_centre = np.mean([x for (x, y) in coordinates])
    lon_centre = np.mean([y for (x, y) in coordinates])
    centre = lat_centre, lon_centre

    m = folium.Map(location=centre, zoom_start=1, zoom_control=False)

    # plot the route line
    folium.PolyLine(route_points, color='red', weight=2).add_to(m)

    # plot each point with a hover tooltip  
    for i, (point, name) in enumerate(zip(coordinates, names)):
        folium.CircleMarker(location=point,
                      tooltip=f'{i}: {name}',
                      radius=2).add_to(m)

    custom_tile_layer = folium.TileLayer(
        tiles='http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
        attr='CartoDB Positron',
        name='Positron',
        overlay=True,
        control=True,
        opacity=0.7  # Adjust opacity to control the level of greying out
    )

    custom_tile_layer.add_to(m)
    folium.LayerControl().add_to(m)

    sw = (np.min([x for (x, y) in coordinates]), np.min([y for (x, y) in coordinates]))
    ne = (np.max([x for (x, y) in coordinates]), np.max([y for (x, y) in coordinates]))
    m.fit_bounds([sw, ne])

    return m

generate_map(coords_ordered, names_ordered, directions).save('gb_cities.html')

结果显示在本文顶部。点击这里查看互动地图。可以放大地图以查看更多细节,并悬停在单个城市上以显示它们在旅游序列中的编号。下面是地图的一部分,显示了经过谢菲尔德的路线(在林肯和切斯特菲尔德之间)。

图片由作者提供。地图数据来源于 OpenStreetMap

第 7 步:可选:创建 GPX 文件

如果需要在现实生活中跟随计算出的路线,例如在具有 GPS 的设备上(如手机或车载导航系统),可以创建 GPX 文件。这不是优化问题的一部分,但如果你想将路线保存到文件中,这是一项可选的附加步骤。GPX 文件是从 directions 变量创建的:

def generate_gpx_file(directions, filename):
    gpx_template = """<?xml version="1.0" encoding="UTF-8"?>
    <gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.topografix.com/GPX/1/1
        http://www.topografix.com/GPX/1/1/gpx.xsd">
        <trk>
            <name>Track</name>
            <trkseg>{}</trkseg>
        </trk>
    </gpx>
    """

    trkseg_template = """
        <trkpt lat="{}" lon="{}"/>
    """

    trkseg_elements = ""
    for point in directions.geometry:
        trkseg_elements += trkseg_template.format(point[1], point[0])

    gpx_data = gpx_template.format(trkseg_elements)

    with open(filename, 'w') as file:
        file.write(gpx_data)

generate_gpx_file(directions, 'gb_cities.gpx')

这个问题的 GPX 文件可以 在这里下载

结论

我们已经看到如何结合以下元素来解决现实世界中的地理旅行推销员问题:

  1. 从 routingpy 库获取的方向和持续时间矩阵,指定适当的profile(交通模式)。

  2. 通过 pyconcorde 包装器使用高效而强大的 Concorde 求解器,以提供最佳路线。

  3. 使用 folium 进行可视化,以创建地图。

上述的自驾游路线是对 79 城旅行推销员问题的一个令人信服的解决方案,根据 Concorde 求解器,它被证明是“最佳的”。然而,由于我们使用的是实际数据,最终结果的准确性取决于输入数据的质量。我们依赖于从 routingpy 获得的点对点时间矩阵能够代表实际情况。实际上,步行、骑车或驾车在不同时间段或一周中的不同日期之间所需的时间会有所不同。这是我们所使用的方法的一种限制。增强最终结果可信度的一种方式是使用替代路线服务来尝试相同的方法。每个路线服务(如 Graphhopper、ORS、Valhalla 等)都有自己的 API,可以用于解决像这里描述的 TSP 问题,并且可以比较不同服务的结果。

尽管解决这样一个问题存在现实世界的限制,上述方法为需要以尽可能高效方式在城市中移动的销售人员或快递员,或者希望在旅行中尽可能多地观光的游客提供了一个良好的起点。通过在互动地图上可视化结果并将路线存储为 GPX 文件,解决方案对最终用户非常有用,而不仅仅是实现代码的数据科学家。

解决逆问题的物理信息深度操作网络:带有代码实现的实用指南

原文:towardsdatascience.com/solving-inverse-problems-with-physics-informed-deeponet-a-practical-guide-with-code-implementation-27795eb4f502

两个带有参数估计和输入函数校准的案例研究

Shuai GuoTowards Data Science Shuai Guo

·发布于Towards Data Science ·21 分钟阅读·2023 年 7 月 17 日

--

愚木混株 cdd20拍摄,来自Unsplash

在我的上一篇博客中,我们深入探讨了物理信息深度操作网络(PI-DeepONet)的概念,并探讨了它为何特别适合于操作符学习,即从输入函数学习到输出函数。我们还将理论转化为代码,实现了一个 PI-DeepONet,该网络即使在遇到未见过的输入强迫配置文件时,也能准确地解决常微分方程(ODE)。

图 1. 操作符将一个函数转换为另一个函数,这是在实际动态系统中经常遇到的概念。操作符学习本质上涉及训练一个神经网络模型以逼近这个基础操作符。一种有前途的方法是DeepONet。 (图片由作者提供)

使用 PI-DeepONet 解决这些前向问题的能力无疑是有价值的。但这就是 PI-DeepONet 能做的全部吗?显然不是!

另一个我们在计算科学和工程中经常遇到的重要问题类别是所谓的逆问题。本质上,这类问题将信息流动从输出反向到输入:输入是未知的,而输出是可观察的,任务是从观察到的输出中估计未知的输入。

图 2. 在前向问题中,目标是通过算子预测已知输入下的输出。在反向问题中,过程是相反的:使用已知的输出估算原始的、未知的输入,通常对底层算子只有部分了解。前向问题和反向问题在计算科学和工程中都很常见。(图像作者提供)

正如你可能已经猜到的,PI-DeepONet 也可以成为解决这些类型问题的超级有用工具。在本博客中,我们将详细探讨如何实现这一点。更具体地,我们将解决两个案例研究:一个是参数估算,另一个是输入函数校准。

本博客旨在自成一体,仅对物理信息(PI-)学习、DeepONet 以及我们的主要关注点 PI-DeepONet 进行简要讨论。有关这些主题的更全面介绍,请随时查看我之前的博客

鉴于此,让我们开始吧!

目录

· 1. PI-DeepONet: 简介

· 2. 问题陈述

· 3. 问题 1:参数估算

∘ 3.1 如何运作

∘ 3.2 实现 PI-DeepONet 管道

∘ 3.3 结果讨论

· 4. 问题 2:输入函数估算

∘ 4.1 解决策略

∘ 4.2 优化常规:TensorFlow

∘ 4.3 优化常规:L-BFGS

· 5. 总结

· 参考文献

1. PI-DeepONet: 简介

正如其名字所示,PI-DeepONet 是两个概念的结合:物理信息学习DeepONet

物理信息学习是机器学习的一种新范式,并在动态系统建模领域获得了特别的关注。其核心思想是将控制微分方程直接融入机器学习模型中,通常通过在损失函数中引入额外的损失项来考虑控制方程的残差。这种学习方法的前提是,以这种方式构建的模型将尊重已知的物理定律,并提供更好的泛化能力、可解释性和可信度。

DeepONet 与此同时属于传统的纯数据驱动建模领域。然而,其独特之处在于 DeepONet 专门设计用于算子学习,即学习从输入函数到输出函数的映射。这种情况在许多动态系统中经常遇到。例如,在一个简单的质量-弹簧系统中,随时间变化的驱动力作为输入函数(时间的函数),而质量的位移则是输出函数(也是时间的函数)。

DeepONet 提出了一种新型网络架构(如图 3 所示),其中分支网络用于转换输入函数的特征,主干网络用于转换时间/空间坐标。这两个网络输出的特征向量通过点积合并。

图 3. DeepONet 的架构。这种方法的独特性在于将分支网络和主干网络分开,分别处理输入函数特征和时间/空间坐标。(作者图片)

现在,如果将物理信息学习的概念叠加到 DeepONet 上,我们得到的就是所谓的 PI-DeepONet。

图 4. 与 DeepONet 相比,PI-DeepONet 包含额外的损失项,如ODE/PDE 残差损失,以及初始条件损失(IC 损失)和边界条件损失(BC 损失)。对于 PI-DeepONet,传统的数据损失是可选的,因为它可以仅通过相关控制方程直接学习基础动态系统的算子。(作者图片)

一旦 PI-DeepONet 经过训练,它可以实时预测给定新输入函数特征的输出函数特征,同时确保预测结果与控制方程一致。正如你所想象的,这使得 PI-DeepONet 成为一个潜在非常强大的工具,适用于各种动态系统建模任务。

然而,在许多其他系统建模场景中,我们可能还需要执行完全相反的操作,即我们知道输出并希望根据观察到的输出和我们对系统动态的先验知识来估计未知的输入。一般来说,这种情况属于逆向建模的范围。这里自然出现了一个问题:我们是否也可以使用 PI-DeepONet 来解决逆向估计问题?

在我们深入探讨之前,让我们首先更准确地制定我们旨在解决的问题。

2. 问题陈述

我们将使用前面博客中讨论的相同 ODE 作为基础模型。之前,我们研究了由以下方程描述的初始值问题:

以初始条件 s(0) = 0 为例。在方程中,u(t)是随时间变化的输入函数,而 s(t)表示系统在时间t的状态。我们之前关注的是解决前向问题,即给定 u(·)预测 s(·)。现在,我们将转变关注点,考虑解决两种类型的逆向问题:

1️⃣ 估计未知的输入参数

让我们从一个简单的逆向问题开始。设想我们的控制 ODE 现在演变成这样:

初始条件 s(0) = 0,a 和 b 是未知数

其中 ab 是两个未知参数。我们的目标是估计 ab 的值,给定观察到的 u(·) 和 s(·) 轮廓。

这类问题属于 参数估计 的范围,其中需要从测量数据中识别系统的未知参数。这类问题的典型示例包括控制工程中的系统识别、计算热传导中的材料热系数估计等。

在我们当前的案例研究中,我们将假设 ab 的真实值均为 0.5。

2️⃣ 估计整个输入函数轮廓

对于第二个案例研究,我们提高了问题的复杂性:假设我们对常微分方程 (ODE) 完全了解(即,我们知道 ab 的确切值)。然而,尽管我们观察了 s(·) 轮廓,但我们尚不知道生成该观察输出函数的 u(·) 轮廓。因此,我们的目标是估计 u(·) 轮廓,给定观察到的 s(·) 轮廓和已知 ODE:

初始条件 s(0) = 0,a=0.5,b=0.5。

由于我们现在的目标是恢复整个输入函数而不是一小部分未知参数,因此这个案例研究将比第一个案例更具挑战性。不幸的是,这类问题本质上是不适定的,需要强有力的正则化来帮助约束解空间。尽管如此,这类问题在多个领域中经常出现,包括环境工程(例如,识别污染源的轮廓)、航空航天工程(例如,校准飞机上的施加载荷)和风工程(例如,风力估计)。

在接下来的两个部分中,我们将逐一讨论这两个案例研究。

3. 问题 1:参数估计

在本节中,我们处理第一个案例研究:估计我们目标 ODE 中的未知参数。我们将首先简要讨论如何使用通用的物理信息神经网络来解决这类问题,然后实施基于 PI-DeepONet 的参数估计管道。之后,我们将其应用于我们的案例研究,并讨论获得的结果。

3.1 工作原理

原始论文 中,Raissi 和合著者概述了使用 PINNs 解决逆问题校准问题的策略:本质上,我们可以简单地将未知参数(在我们当前的案例中,即参数 ab)设为 可训练参数,并与神经网络的权重和偏置一起优化这些未知参数,以最小化损失函数。

当然,关键在于构造损失函数:作为一种物理信息学习方法,它不仅包含一个数据不匹配项,该项衡量网络预测输出与观察数据之间的差异,还包含一个物理信息正则化项,该项使用神经网络的输出(及其导数)和当前的 ab 参数估计值来计算残差(即微分方程左右两侧的差异)。

现在,当我们执行这种联合优化时,我们实际上是在寻找 ab 值,这些值会导致网络的输出同时适应观察到的数据并满足控制微分方程。当损失函数达到最小值时(即训练收敛),我们获得的 ab 的最终值就是实现这种平衡的值,因此它们就是未知参数的估计值。

图 5. 在使用 PI-DeepONet 时,未知参数 ab 与 DeepONet 模型的权重和偏置一起优化。当训练收敛时,最终得到的 ab 值即为它们的估计值。(图像来自作者)

3.2 实现 PI-DeepONet 流水线

说够理论,接下来是代码部分 💻

我们从 PI-DeepONet 的定义开始:

def create_model(mean, var, a_init=None, b_init=None, trainable=None, verbose=False):
    """Definition of a DeepONet with fully connected branch and trunk layers.

    Args:
    ----
    mean: dictionary, mean values of the inputs
    var: dictionary, variance values of the inputs
    a_init: float, initial value for parameter a
    b_init: float, initial value for parameter b
    trainable: boolean, indicate whether the parameters a and b will be updated during training
    verbose: boolean, indicate whether to show the model summary

    Outputs:
    --------
    model: the DeepONet model
    """

    # Branch net
    branch_input = tf.keras.Input(shape=(len(mean['forcing'])), name="forcing")
    branch = tf.keras.layers.Normalization(mean=mean['forcing'], variance=var['forcing'])(branch_input)
    for i in range(3):
        branch = tf.keras.layers.Dense(50, activation="tanh")(branch)

    # Trunk net
    trunk_input = tf.keras.Input(shape=(len(mean['time'])), name="time")
    trunk = tf.keras.layers.Normalization(mean=mean['time'], variance=var['time'])(trunk_input)   
    for i in range(3):
        trunk = tf.keras.layers.Dense(50, activation="tanh")(trunk)

    # Merge results 
    dot_product = tf.reduce_sum(tf.multiply(branch, trunk), axis=1, keepdims=True)

    # Add the bias
    dot_product_with_bias = BiasLayer()(dot_product)

    # Add a & b trainable parameters
    output = ParameterLayer(a_init, b_init, trainable)(dot_product_with_bias)

    # Create the model
    model = tf.keras.models.Model(inputs=[branch_input, trunk_input], outputs=output)

    if verbose:
        model.summary()

    return model 

在上述代码中:

  1. 我们首先定义了主干网络和分支网络(它们都是简单的全连接网络)。两个网络生成的特征向量通过点积合并在一起。

  2. 我们通过在点积结果上添加一个偏置项来进行下一步,这通过定义一个自定义的 BiasLayer() 实现。该策略可以提高预测准确性,正如 原始 DeepONet 论文 中所指出的。

  3. 这里是使我们的模型能够解决参数估计问题的主要变化:我们将 ab 添加到神经网络模型参数集合中。这样,当我们将 trainable 设置为 true 时,ab 将与神经网络的其他常规权重和偏置一起进行优化。技术上,我们通过定义一个自定义层来实现这个目标:

class ParameterLayer(tf.keras.layers.Layer):

    def __init__(self, a, b, trainable=True):
        super(ParameterLayer, self).__init__()
        self._a = tf.convert_to_tensor(a, dtype=tf.float32)
        self._b = tf.convert_to_tensor(b, dtype=tf.float32)
        self.trainable = trainable

    def build(self, input_shape):
        self.a = self.add_weight("a", shape=(1,), 
                                 initializer=tf.keras.initializers.Constant(value=self._a),
                                 trainable=self.trainable)
        self.b = self.add_weight("b", shape=(1,), 
                                 initializer=tf.keras.initializers.Constant(value=self._b),
                                 trainable=self.trainable)

    def get_config(self):
        return super().get_config()

    @classmethod
    def from_config(cls, config):
        return cls(**config)

请注意,这一层除了将这两个参数引入为模型属性外没有其他功能。

接下来,我们定义计算常微分方程残差的函数,这将作为物理信息损失项:

@tf.function
def ODE_residual_calculator(t, u, u_t, model):
    """ODE residual calculation.

    Args:
    ----
    t: temporal coordinate
    u: input function evaluated at discrete temporal coordinates
    u_t: input function evaluated at t
    model: DeepONet model

    Outputs:
    --------
    ODE_residual: residual of the governing ODE
    """

    with tf.GradientTape() as tape:
        tape.watch(t)
        s = model({"forcing": u, "time": t})

    # Calculate gradients
    ds_dt = tape.gradient(s, t)

    # ODE residual
    ODE_residual = ds_dt - model.layers[-1].a*u_t - model.layers[-1].b

    return ODE_residual

请注意,我们使用 model.layers[-1] 来检索我们之前定义的参数层。这将为我们提供 ab 值,以计算常微分方程的残差。

接下来,我们定义计算总损失对参数(包括常规权重和偏置以及未知参数 ab)的梯度的逻辑。这将为我们进行梯度下降以训练模型做好准备:

@tf.function
def train_step(X, y, X_init, IC_weight, ODE_weight, data_weight, model):
    """Calculate gradients of the total loss with respect to network model parameters.

    Args:
    ----
    X: training dataset for evaluating ODE residuals
    y: target value of the training dataset
    X_init: training dataset for evaluating initial conditions
    IC_weight: weight for initial condition loss
    ODE_weight: weight for ODE loss
    data_weight: weight for data loss
    model: DeepONet model

    Outputs:
    --------
    ODE_loss: calculated ODE loss
    IC_loss: calculated initial condition loss
    data_loss: calculated data loss
    total_loss: weighted sum of ODE loss, initial condition loss, and data loss
    gradients: gradients of the total loss with respect to network model parameters.
    """

    with tf.GradientTape() as tape:
        tape.watch(model.trainable_weights)

        # Initial condition prediction
        y_pred_IC = model({"forcing": X_init[:, 1:-1], "time": X_init[:, :1]})

        # Equation residual
        ODE_residual = ODE_residual_calculator(t=X[:, :1], u=X[:, 1:-1], u_t=X[:, -1:], model=model)

        # Data loss
        y_pred_data = model({"forcing": X[:, 1:-1], "time": X[:, :1]})

        # Calculate loss
        IC_loss = tf.reduce_mean(keras.losses.mean_squared_error(0, y_pred_IC))
        ODE_loss = tf.reduce_mean(tf.square(ODE_residual))
        data_loss = tf.reduce_mean(keras.losses.mean_squared_error(y, y_pred_data))

        # Weight loss
        total_loss = IC_loss*IC_weight + ODE_loss*ODE_weight + data_loss*data_weight

    gradients = tape.gradient(total_loss, model.trainable_variables)

    return ODE_loss, IC_loss, data_loss, total_loss, gradients

请注意,我们的损失函数由三个部分组成:初始条件损失、ODE 残差和数据损失。对于正向问题,数据损失是可选的,因为模型可以仅根据给定的微分方程直接学习基础算子。然而,对于反向问题(如当前的参数估计情况),结合数据损失是必要的,以确保我们找到正确的 ab 值。在我们当前的案例中,仅需将所有权重,即 IC_weightODE_weightdata_weight 设置为 1 即可。

现在我们准备好定义主要的训练逻辑:

# Set up training configurations
n_epochs = 100
IC_weight= tf.constant(1.0, dtype=tf.float32)   
ODE_weight= tf.constant(1.0, dtype=tf.float32)
data_weight= tf.constant(1.0, dtype=tf.float32)

# Initial value for unknown parameters
a, b = 1, 1

# Optimizer
optimizer = keras.optimizers.Adam(learning_rate=1e-3)

# Instantiate the PINN model
PI_DeepONet = create_model(mean, var, a_init=a, b_init=b, trainable=True)
PI_DeepONet.compile(optimizer=optimizer)

# Start training process
for epoch in range(1, n_epochs + 1):  
    print(f"Epoch {epoch}:")

    for (X_init, _), (X, y) in zip(ini_ds, train_ds):

        # Calculate gradients
        ODE_loss, IC_loss, data_loss, total_loss, gradients = train_step(X, y, X_init, 
                                                                        IC_weight, ODE_weight,
                                                                        data_weight, PI_DeepONet)
        # Gradient descent
        PI_DeepONet.optimizer.apply_gradients(zip(gradients, PI_DeepONet.trainable_variables))

    # Re-shuffle dataset
    ini_ds = tf.data.Dataset.from_tensor_slices((X_train_ini, y_train_ini))
    ini_ds = ini_ds.shuffle(5000).batch(ini_batch_size)

    train_ds = tf.data.Dataset.from_tensor_slices((X_train, y_train))
    train_ds = train_ds.shuffle(100000).batch(col_batch_size) 

在这里,我们将 ab 的初始值都设置为 1。请记住,ab 的真实值实际上是 0.5*。接下来,让我们训练 PI-DeepONet 模型,看看它是否能够恢复 ab 的真实值。

请注意,我们讨论的代码仅是从完整的训练/验证逻辑中提取的关键片段。要查看完整代码,请查看notebook

3.3 结果讨论

要使用 PI-DeepONet 估计未知的 ODE 参数,我们首先需要生成训练数据集。在我们当前的案例中,数据生成分为两个步骤:首先,我们使用零均值的高斯过程生成 u(·) 曲线(即输入函数),其具有径向基函数(RBF)核。之后,我们在目标 ODE 上运行 scipy.integrate.solve_ivp(将 ab 都设置为 0.5)以计算相应的 s(·) 曲线(即输出函数)。下图展示了用于训练的三个随机样本。

图 6. 从训练数据集中随机选择的三个样本用于说明。上排显示了 u(·) 曲线,下排显示了通过运行 ODE 求解器计算的对应 s(·) 曲线。(图片由作者提供)

一旦训练数据集准备好,我们就可以启动 PI-DeepONet 的训练过程。下图显示了损失的演变,这表明模型已经得到了适当的训练,并能够很好地拟合数据和 ODE。

图 7. 损失收敛图。(图片由作者提供)

训练 PI-DeepONet 并不是终点。我们的最终目标是估计 ab 的值。下图展示了未知参数的演变。我们可以清楚地看到,PI-DeepONet 正在发挥作用,并准确地估计了 ab 的真实值。

图 8. 我们的未知参数 ab 逐渐远离指定的初始值,并收敛到它们的真实值。这表明 PI-DeepONet 能够对具有函数输入的微分方程进行反向参数校准。(图片由作者提供)

我们可以进一步测试估计结果对初始值的敏感性。下图显示了在不同初始值(a_init=0.2,b_init=0.8)下参数的演变:

图 9. 对于不同的初始值集,我们开发的 PI-DeepONet 模型能够准确估计 a 和 b 的真实值。(图片由作者提供)

总体而言,我们可以看到 PI-DeepONet 模型能够在 ODE 的输入为函数时进行参数校准。

4. 问题 2:输入函数估计

该升级我们的游戏时间到了!在本节中,我们将解决一个更具挑战性的案例研究:估计输入函数 u(·)的整个轮廓。类似于前一节,我们将从简要讨论解决方案策略开始,然后实施提出的策略来解决我们的目标问题。我们将探讨两种不同的策略:一种使用原生 TensorFlow,另一种使用 L-BFGS 优化算法。

4.1 解决方案策略

为了制定我们的解决方案策略,我们需要考虑两件事:

1️⃣ 优化效率

从根本上讲,我们的目标反问题要求优化输入轮廓 u(·),使得预测的 s(·)与观察到的 s(·)匹配。假设我们可以给定 u(·)计算 s(·),许多现成的优化器可以用来实现我们的目标。现在的问题是,给定 u(·)我们如何计算 s(·)?

传统的方法是使用标准的数值 ODE 求解器来预测给定输入函数 u(·)的输出函数 s(·)。然而,优化过程通常涉及多个迭代。因此,使用数值 ODE 求解器来计算 s(·)的方法会太低效,因为在每次优化循环迭代中,u(·)会被更新,并且需要计算新的 s(·)。这就是 PI-DeepONet 发挥作用的地方。

作为第一步,我们需要一个完全训练好的 PI-DeepONet。由于我们对目标 ODE 有完全的了解(回顾一下在这个案例研究中,我们假设知道ab的真实值),我们可以仅基于 ODE 残差损失轻松地训练一个 PI-DeepONet。

一旦 PI-DeepONet 训练完成,我们可以冻结其权重和偏置,将其视为一个快速的“替代”模型,可以高效地计算给定任何 u(·)的 s(·),并将这个训练好的 PI-DeepONet 嵌入到优化例程中。由于 PI-DeepONet 的推理时间可以忽略不计,计算成本可以大大降低。同时,训练后的 PI-DeepONet 模型将是完全可微的。这表明我们能够提供优化目标函数相对于 u(·)的梯度,从而利用基于梯度的优化算法的效率。这两个方面使得迭代优化变得可行。

2️⃣ 优化量

当我们谈论估计 u(·) 的轮廓时,我们并不是试图估计 u(·) 的符号函数形式。而是估计离散的 u(·) 值,这些值是在固定时间坐标 t₁、t₂ 等上评估的。这也与我们如何将 u(·) 输入到 PI-DeepONet 中的方式一致。

通常,我们需要有大量离散的 u(·) 值来充分描述 u(·) 的轮廓。因此,我们的优化问题将是高维的,因为有许多参数需要优化。这种类型的问题本质上是病态的:仅仅找到一个能够生成匹配的 s(·) 的 u(·) 很可能不是一个很强的约束,因为很容易存在多个 u(·) 能生成完全相同的 s(·) 轮廓。我们需要更强的正则化来帮助约束解空间。

那么我们该怎么做呢?实际上,我们可以利用已知 ODE 带来的约束。简单来说,我们的目标是找到一个 u(·),不仅生成与观察到的 s(·) 匹配的 s(·),而且满足规定 u(·) 和 s(·) 之间关系的已知 ODE

图 10. 优化策略的示意图。(图像由作者提供)

现在这些问题已经澄清,让我们继续编写优化程序。

4.2 优化程序:TensorFlow

如我们之前讨论的,我们从训练一个观察已知 ODE 的 PI-DeepONet 开始。我们可以重用为第一个案例研究开发的代码。我们需要引入的唯一更改是:

  1. a_initb_init 设置为 0.5,因为这些是 ab 的真实值。

  2. ParameterLayertrainable 标志设置为 False。这将防止在反向传播期间更新 ab 的值。

  3. data_weight 设置为 0,因为我们不需要配对的 u(·)-s(·) 来进行训练。PI-DeepONet 可以仅基于 ODE 残差损失进行训练,正如我们在之前的博客中所展示的那样。

下图展示了训练过程:损失值已经收敛,PI-DeepONet 已经得到适当训练。

图 11. PI-DeepONet 在没有配对输入输出数据的情况下的损失收敛。(图像由作者提供)

接下来,我们定义目标函数及其相对于 u(·) 的梯度,以用于我们的优化过程:

@tf.function
def gradient_u(model, u, s_target):
    """ Calculate the gradients of the objective function with respect to u(·).

    Args:
    ----
    model: the trained PI-DeepONet model
    u: the current u()
    s_target: the observed s()

    Outputs:
    --------
    obj: objective function
    gradients: calculated gradients
    """

    with tf.GradientTape() as tape:
        tape.watch(u)

        # Initial condition loss
        y_pred_IC = model({"forcing": u, "time": 0})
        IC_loss = tf.reduce_mean(keras.losses.mean_squared_error(0, y_pred_IC))

        # ODE residual
        ODE_residual = ODE_residual_calculator(t=tf.reshape(tf.linspace(0.0, 1.0, 100), (-1, 1)), 
                                               u=tf.tile(tf.reshape(u, (1, -1)), [100, 1]), 
                                               u_t=tf.reshape(u, (-1, 1)), model=model)
        ODE_loss = tf.reduce_mean(tf.square(ODE_residual))

        # Predict s() with the current u()
        y_pred = model({"forcing": tf.tile(tf.reshape(u, (1, -1)), [100, 1]), 
                        "time": tf.reshape(tf.linspace(0.0, 1.0, 100), (-1, 1))})

        # Data loss
        data_loss = tf.reduce_mean(keras.losses.mean_squared_error(s_target, y_pred))

        # Objective function
        obj = IC_loss + ODE_loss + data_loss

    # Gradient descent
    gradients = tape.gradient(obj, u)

    return obj, gradients

如你所见,我们主要重新利用了在第一个案例研究中开发的 train_step() 函数来定义优化的目标函数。在梯度计算方面,两个函数的主要区别在于这里我们计算的是目标函数(即总损失)相对于输入 u(·) 的梯度,而 train_step() 计算的是总损失相对于神经网络模型参数的梯度。这很合理,因为之前我们想要优化神经网络参数(即权重和偏差),而在这里我们希望优化输入 u(·)。

还需要注意的是,我们将 u(·) 离散化为在 [0, 1] 区间内均匀分布的 100 个点。因此,我们手头的 u(·) 估计任务本质上是优化这 100 个离散的 u(·) 值。

接下来,我们定义了优化 u(·) 的主要逻辑:

def optimize_u(model, initial_u, s_target, learning_rate=5e-3, opt_steps=10000):
    """ Determine u() that generates the observed s().

    Args:
    ----
    model: the trained PI-DeepONet model
    initial_u: initial guess for u()
    s_target: the observed s()
    learning_rate: learning rate for the optimizer
    opt_steps: number of optimization iterations

    Outputs:
    --------
    u: the optimized u()
    u_hist: intermedian u results
    objective_func_hist: value history of the objective function
    """

    # Optimizer
    optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)

    # Preparation
    u_var = tf.Variable(initial_u, dtype=tf.float32)
    objective_func_hist = []
    u_hist = []

    for step in range(opt_steps):

        # Calculate gradients
        obj, gradients = gradient_u(model, u_var, s_target)

        # Gradient descent
        optimizer.apply_gradients([(gradients, u_var)])

        # Record results
        objective_func_hist.append(obj.numpy())
        u_hist.append(u_var.numpy())

        # Show progress
        if step % 500 == 0:
            print(f'Step {step} ==> Objective function = {obj.numpy()}')

    # Optimized u()
    u = u_var.numpy()

    return u, objective_func_hist, u_hist

随后,我们可以启动优化过程。对于这个案例研究,我们随机生成一个 u(·) 配置,并使用数值求解器计算其对应的 s(·) 配置。然后,我们将观察到的 s(·) 分配给 s_target,并测试我们的优化算法是否确实能够识别我们用来生成 s(·) 的 u(·)。

在初始化时,我们简单地使用全零的 u(·) 配置,并将其分配给 initial_u。下图显示了目标函数的收敛情况。

图 12. 我们使用固定学习率(5e-3)的 Adam 优化器来优化 u(·)。(图像由作者提供)

下图显示了 u(·) 配置的演变。我们可以看到,随着迭代次数的增加,u(·) 配置逐渐收敛到真实的 u(·)。

图 13. 在逆校准过程中,u(·) 逐渐收敛到真实的 u(·),这表明 PI-DeepONet 方法能够成功地执行输入函数配置的逆校准。(图像由作者提供)

然后,我们可以执行通常的前向模拟,以获得给定优化 u(·) 的预测 s(·)。前向模拟通过数值 ODE 求解器和训练过的 PI-DeepONet 进行。我们可以看到,预测的 s(·) 紧密跟随实际值。

图 14. 向前模拟以计算给定优化 u(·) 的 s(·)。预测结果与实际值非常吻合。(图像由作者提供)

我们还可以进一步测试我们的优化策略是否适用于其他 u(·) 配置。以下图表展示了我们在测试中使用的另外六个随机 u(·) 配置:

图 15. 对于不同的 u(·) 配置,我们使用全零初始化运行了相同的基于 PI-DeepONet 的优化例程。结果表明我们的策略成功地提供了令人满意的结果。(图像由作者提供)

总体而言,结果显示 PI-DeepONet 方法能够成功地对整个输入函数轮廓进行反向校准。

4.3 优化例程:L-BFGS

我们上面开发的优化例程完全在 TensorFlow 环境中操作。实际上,我们还可以利用二阶优化方法,如 SciPy 中实现的 L-BGFS,来实现我们的目标。在本节中,我们将探讨如何将我们开发的 PI-DeepONet 集成到 L-BGFS 优化工作流中。

如果你想了解更多关于 L-BFGS 方法的信息,可以查看这篇博客文章:基于 L-BFGS 方法的数值优化

首先,我们定义了 L-BFGS 优化器的目标函数。目标函数再次是数据损失和 ODE 残差损失的组合:

def obj_fn(u_var, model, s_target, convert_to_numpy=False):
    """ Calculate objective function.

    Args:
    ----
    u_var: u()
    model: the trained PI-DeepONet model
    s_target: the observed s()
    convert_to_numpy: boolean, indicate whether to convert the 
                      calculated objective value to numpy

    Outputs:
    --------
    obj: objective function
    """

    # Initial condition prediction
    y_pred_IC = model({"forcing": u_var, "time": 0})

    # ODE residual
    ODE_residual = ODE_residual_calculator(t=tf.reshape(tf.linspace(0.0, 1.0, 100), (-1, 1)), 
                                           u=tf.tile(tf.reshape(u_var, (1, -1)), [100, 1]), 
                                           u_t=tf.cast(tf.reshape(u_var, (-1, 1)), tf.float32), model=model)

    # Predict the output with the current inputs
    y_pred = model({"forcing": tf.tile(tf.reshape(u_var, (1, -1)), [100, 1]), 
                    "time": tf.reshape(tf.linspace(0.0, 1.0, 100), (-1, 1))})

    # Compute the data loss
    data_loss = tf.reduce_mean(keras.losses.mean_squared_error(y_target, y_pred))

    # Calculate other losses
    IC_loss = tf.reduce_mean(keras.losses.mean_squared_error(0, y_pred_IC))
    ODE_loss = tf.reduce_mean(tf.square(ODE_residual))

    # Obj function
    obj = IC_loss + ODE_loss + data_loss
    obj = obj if not convert_to_numpy else obj.numpy().astype(np.float64)

    return obj

此外,我们需要定义 obj_fn 的两个不同实例:一个用于返回 Numpy 对象,另一个用于返回 TensorFlow 对象。

def obj_fn_numpy(u_var, model, s_target):
    return obj_fn(u_var, model, s_target, convert_to_numpy=True)

def obj_fn_tensor(u_var, model, s_target):
    return obj_fn(u_var, model, s_target, convert_to_numpy=False)

obj_fn_numpy 将由 SciPy 的 L-BFGS 用于计算主要的目标函数,而 obj_fn_tensor 将由 TensorFlow 用于计算目标函数关于 u(·) 的梯度,如下所示:

def grad_fn(u, model, s_target):
    u_var = tf.Variable(u, dtype=tf.float32)

    with tf.GradientTape() as tape:
        tape.watch(u_var)
        obj = obj_fn_tensor(u_var, model, s_target)

    # Calculate gradients
    grads = tape.gradient(obj, u_var)

    return grads.numpy().astype(np.float64)

现在我们准备定义主要的优化逻辑:

from scipy.optimize import minimize

def optimize_u(model, initial_u, s_target, opt_steps=1000):
    """Optimize the inputs to a model to achieve a target output.

    Args:
    ----
    model: the trained PI-DeepONet model
    initial_u: initial guess for u()
    s_target: the observed s()
    opt_steps: number of optimization iterations

    Outputs:
    --------
    res.x: the optimized u
    """

    # L-BFGS optimizer 
    res = minimize(fun=obj_fn_numpy, x0=initial_u, args=(model, s_target),
                   method='L-BFGS-B', jac=grad_fn, options={'maxiter': opt_steps, 'disp': True})

    return res.x

为了测试 L-BFGS 算法的有效性,我们采用了前一小节开始时研究的相同 u(·) 和 s(·)。我们还将初始 u(·) 设置为全零轮廓。经过 1000 次 L-BFGS 迭代,我们获得了以下优化结果:

图 16. 校准后的 u(·) 和预测的 s(·)。(图片作者提供)

我们可以看到,L-BFGS 算法也取得了预期的结果,成功估计了 u(·) 轮廓。我们进一步检查了六个不同的 u(·) 轮廓,并获得了类似的结果。

图 17. 使用 L-BFGS 算法对不同 u(·) 轮廓进行校准的结果。(图片作者提供)

5. 重点总结

在这篇博客中,我们研究了如何使用 PI-DeepONet,这是一种结合了物理信息学习和算子学习优势的新型网络架构,来解决反问题。我们研究了两个案例:

1️⃣ 参数估计

我们的目标是给定观测到的输入函数 u(·) 和输出函数 s(·),估计未知的 ODE 参数。我们的策略是将这些未知参数作为可训练参数纳入神经网络,并与神经网络的权重和偏置一起优化。

2️⃣ 输入函数校准

我们的目标是给定完全已知的 ODE 和观察到的 s(·)来估计整个 u(·)曲线。我们的策略是首先基于已知的 ODE 训练一个 PI-DeepONet,作为快速的替代模型,然后优化离散化的 u(·)值,以使生成的 s(·)不仅与观察到的真实情况匹配,而且也满足连接 u(·)和 s(·)的已知 ODE。

对于这两个案例研究,我们已经看到 PI-DeepONet 方法产生了令人满意的结果,这表明该方法可以有效地解决正向和逆向问题,从而作为动态系统建模的有前途的工具。

逆向问题在计算科学和工程中提出了独特的挑战,而我们在本博客中仅仅触及了这一领域的表面。对于许多实际应用,我们还需要进一步考虑其他方面:

1️⃣ 不确定性量化(UQ)

在本博客中,我们简单地假设观察到的输入和输出函数值是完美的。然而,在实际场景中,输入和输出数据可能会受到噪声的污染。理解这种不确定性如何在我们的模型中传播对于做出可靠的估计至关重要。

2️⃣ 建模误差

在本博客中,我们假设我们的 ODE 完美地描述了系统的动态。然而,在实际场景中,这一假设很可能不成立:微分方程可能只是对真实物理过程的近似,还有一些物理现象没有被方程捕捉到。如何正确考虑这些引入的建模误差可能是一个具有挑战性但又有趣的方面。

3️⃣ 扩展到 PDE

在本博客中,我们展示了解决逆向问题的工作流程,仅考虑了一个简单的 ODE。然而,许多实际系统由具有更多空间/时间坐标和复杂输入/输出功能特征的 PDE 支配。当我们将开发的方法扩展到 PDE 时,可能会遇到更多挑战(例如,计算效率可能成为主要问题,不仅在训练 PI-DeepONet 时,还在进行逆向标定优化时)。

恭喜你看到这里!如果你觉得我的内容有用,可以在这里请我喝杯咖啡🤗 非常感谢你的支持!

你可以在这里找到带有完整代码的配套笔记本 💻

要了解更多关于正向建模的 PI-DeepONet:通过物理信息深度操作网络进行的操作学习

要了解物理信息学习的最佳实践:揭示物理信息神经网络的设计模式

你也可以订阅我的通讯或在Medium上关注我。

参考文献

[1] 王等,利用物理信息深度网络学习参数化偏微分方程的解算子。arXiv,2021 年。

像我们一样学习的机器:解决泛化-记忆困境

原文:towardsdatascience.com/solving-machine-learnings-generalization-memorization-dilemma-3-promising-paradigms-ab9c236add3e

解决机器学习中最重要问题的 3 种范式

Samuel FlenderTowards Data Science Samuel Flender

·发布于 Towards Data Science ·7 分钟阅读·2023 年 4 月 5 日

--

(Midjourney)

机器学习的“圣杯”是能够构建既能记忆训练数据中的已知模式,又能泛化到未知模式的系统。

之所以称之为圣杯,是因为这也是我们人类学习的方式。你可以在一张旧照片中识别出你的奶奶,但即使你从未见过 Xoloitzcuintli,你也能将其识别为狗。如果没有记忆,我们就得不断从头再学一遍一切;如果没有泛化,我们将无法适应不断变化的世界。为了生存,我们需要两者兼备。

传统的统计学习理论告诉我们这是不可能的:模型要么能够很好地泛化,要么能够很好地记忆,但不能兼顾两者。这就是著名的偏差-方差权衡,这是我们在标准机器学习课程中首先学到的东西。

那么我们如何构建这样的通用学习系统呢?圣杯是否触手可及?

在这篇文章中,让我们深入探讨文献中的 3 种范式,

  1. 先泛化,后记忆

  2. 同时泛化和记忆

  3. 机器泛化,人类记忆

让我们开始吧。

1 — 先泛化,后记忆

BERT 通过引入预训练/微调范式彻底改变了机器学习:在大量文本数据上进行无监督的预训练后,该模型可以在特定的下游任务上快速微调,所需的标签相对较少。

惊人的是,这种预训练/微调方法解决了泛化/记忆化问题。BERT 能够进行泛化和记忆,正如来自帝国理工学院的 Michael Tänzer 和他的合作者在 2022 年的 论文 中展示的那样。

特别是,作者展示了在微调过程中,BERT 学习的 3 个不同的阶段:

  1. 拟合(训练轮次 1):模型学习简单的通用模式,尽可能解释训练数据。在此阶段,训练和验证性能都在提高。

  2. 设置(训练轮次 2–5):没有更多简单的模式可以学习。训练和验证性能饱和,在学习曲线上形成一个平台。

  3. 记忆化(训练轮次 6+):模型开始记忆训练集中的特定示例,包括噪声,这提高了训练性能但降低了验证性能。

他们是如何发现这一点的?通过从无噪声的训练集(CoNLL03,一个命名实体识别基准数据集)开始,然后逐渐引入更多的人工标签噪声。比较不同噪声量的学习曲线清楚地揭示了 3 个不同的阶段:更多噪声会导致第 3 阶段的下降更加陡峭。

微调 BERT 显示了 3 个不同的学习阶段。图来自 Tänzer 等人,“预训练语言模型中的记忆化与泛化” (link)

Tänzer 等人还表明,BERT 的记忆化需要重复:BERT 只有在看到特定训练示例一定次数后才会记住该示例。这可以从人为引入噪声的学习曲线中推断出来:它是一个阶跃函数,每个训练轮次都会改善。换句话说,在第 3 阶段,如果我们让 BERT 训练足够多的轮次,它最终可以记住整个训练集。

总之,BERT 似乎首先进行泛化,然后进行记忆,这从其在微调过程中观察到的 3 个不同的学习阶段可以得到证实。事实上,还可以证明这种行为是预训练的直接结果:Tänzer 等人展示了一个随机初始化的 BERT 模型不会共享相同的 3 个学习阶段。这导致了预训练/微调范式可能是解决泛化/记忆化困境的一个可能方案的结论。

2 — 同时进行泛化和记忆

让我们离开自然语言处理的世界,进入 推荐系统 的世界。

在现代推荐系统中,同时具备记忆和泛化的能力至关重要。例如,YouTube 想向你展示与你过去观看的视频类似的内容(记忆),但也展示一些你甚至不知道自己会喜欢的新视频(泛化)。没有记忆你会感到沮丧,没有泛化你会感到无聊。

目前最好的推荐系统需要同时做到这两点。但是怎么做呢?

在 2016 年的论文中,Heng-Tze Cheng 和来自 Google 的合作者提出了他们所称的“宽广而深度学习”来解决这个问题。关键思想是构建一个包含深度组件(具有嵌入输入的深度神经网络)用于泛化以及宽广组件(具有大量稀疏输入的线性模型)用于记忆的单一神经网络。作者在 Google Play 商店的推荐中演示了这种方法的有效性,该商店向用户推荐应用程序。

深度组件的输入是密集特征以及分类特征的嵌入,例如用户语言、用户性别、印象中的应用程序、已安装的应用程序等。这些嵌入被随机初始化,然后在模型训练过程中与神经网络中的其他参数一起调整。

网络宽广组件的输入是细粒度的交叉特征,例如

AND(user_installed_app=netflix, impression_app=hulu”),

如果用户安装了 Netflix 且印象中的应用程序是 Hulu,则其值为 1。

很容易看出为什么宽广组件能实现一种记忆:如果 99% 的安装了 Netflix 的用户也最终安装了 Hulu,宽广组件将能够学习这一信息,而它可能会在深度组件中丢失。 Cheng 等人认为,同时拥有宽广和深度组件确实是达到最佳性能的关键。

实验结果实际上证实了作者的假设。宽广&深度模型在 Google Play 商店的在线获取增益方面优于仅宽广模型(提高了 2.9%)和仅深度模型(提高了 1%)。这些实验结果表明,“宽广&深度”是解决泛化/记忆困境的另一种有前途的范式。

3 — 让机器泛化,让人类记忆

Tänzer 和 Cheng 提出了仅用机器解决泛化/记忆困境的方法。然而,机器很难记住单个示例:Tänzer 等人发现 BERT 需要至少 25 个实例来学习预测一个类别,且需要 100 个示例才能“有一定准确性”地预测。

退一步说,我们不必让机器完成所有的工作。与其对抗机器的记忆失败,不如接受它?为什么不构建一个结合了机器学习和人类专业知识的混合系统呢?

这正是 Chimera 背后的理念,Chimera 是沃尔玛用于大规模电子商务商品分类的生产系统,由 Chong Sun 和沃尔玛实验室的合作者在 2014 年的论文中提出。Chimera 的前提是,仅凭机器学习不足以处理大规模的商品分类,因为存在大量训练数据稀缺的边缘案例。

例如,沃尔玛可能同意试验性地从新供应商那里采购有限数量的新产品。由于缺乏足够的训练数据,机器学习系统可能无法准确分类这些产品。然而,人类分析师可以编写规则来准确覆盖这些情况。这些基于规则的决策随后可以用于模型训练,使模型在经过一段时间后能够适应新模式。

作者总结道:

我们广泛使用机器学习和手工编写的规则。我们系统中的规则不是“锦上添花”。它们对实现预期性能至关重要,并且为领域分析师提供了一种快速有效的方式将反馈输入系统。据我们所知,我们是第一个描述一个工业级系统的团队,在其中机器学习和规则作为一流公民共存。

唉,这种共存也可能是解决概括/记忆问题的关键。

附录:像我们一样学习的系统

总结一下。构建能够同时记忆已知模式和对未知模式进行概括的系统是机器学习的圣杯。到目前为止,还没有人完全破解这个问题,但我们已经看到了一些有前景的方向:

  • BERT 已被证明在微调过程中先进行概括,再进行记忆,这种能力得益于其之前的预训练。

  • Wide & Deep 神经网络被设计为同时进行概括(使用深度组件)和记忆(使用宽度组件),在 Google Play 商店推荐中优于仅宽度或仅深度网络。

  • 沃尔玛的混合生产系统 Chimera 利用人类专家为其机器学习模型无法记忆的边缘案例编写规则。通过将这些基于规则的决策重新加入训练数据中,随着时间的推移,机器学习模型可以赶上,但最终机器学习和规则作为一流公民共存。

这实际上只是对现状的一个小小的窥视。任何工业机器学习团队都需要在某种程度上解决记忆/概括困境。如果你从事机器学习工作,你很可能最终也会遇到这个问题。

解决这个问题的最终目标不仅是使我们能够构建更为优越的机器学习系统。它还将使我们能够构建更像我们学习的系统

[## 不想依赖 Medium 的算法?请注册。

不想依赖 Medium 的算法?请注册。通过注册我的电子邮件,确保你不会错过我的下一篇文章…

medium.com](https://medium.com/@samuel.flender/subscribe?source=post_page-----ab9c236add3e--------------------------------)

使用离策略蒙特卡洛控制解决强化学习赛道练习

原文:towardsdatascience.com/solving-reinforcement-learning-racetrack-exercise-building-the-environment-33712602de0c?source=collection_archive---------0-----------------------#2023-07-23

Tingsong OuTowards Data Science Tingsong Ou

·

关注 发表在 Towards Data Science ·10 min read·2023 年 7 月 23 日

--

图片由 Midjourney 使用付费订阅生成,符合一般商业条款 [1]。

介绍

在书籍 强化学习:导论 第二版(第 112 页)离策略蒙特卡洛控制 一节中,作者给我们留下了一个有趣的练习:使用加权重要性采样离策略蒙特卡洛方法找到在两条赛道上最快的行驶方式。这个练习非常全面,要求我们考虑和构建强化学习任务的几乎所有组件,如环境、代理、奖励、动作、终止条件和算法。解决这个练习很有趣,并帮助我们建立对算法与环境之间互动的扎实理解,正确的 episodic 任务定义的重要性,以及价值初始化如何影响训练结果。通过这篇文章,我希望与所有对强化学习感兴趣的人分享我对这个练习的理解和解决方案。

练习要求

如上所述,这个练习要求我们找到一种策略,使赛车从起始线尽可能快速地驶向终点线,而不撞到碎石或驶出赛道。在仔细阅读练习描述后,我列出了完成这项任务的关键点:

  • 地图表示:在此背景下,地图实际上是以 (row_index, column_index) 为坐标的二维矩阵。每个单元格的值表示该单元格的状态;例如,我们可以用 0 来描述碎石,用 1 来表示赛道表面,用 0.4 表示起始区域,用 0.8 表示终点线。任何超出矩阵的行列索引都可以视为越界。

  • 汽车表示:我们可以直接使用矩阵的坐标来表示汽车的位置;

  • 速度和控制:速度空间是离散的,包括水平和垂直速度,可以表示为一个元组 (row_speed, col_speed)。两个轴上的速度限制为 (-5, 5),并且每个轴在每一步递增 +1、0 或 -1;因此,每一步有总共 9 种可能的动作。字面上,除了起始线外,速度不能同时为零,垂直速度(或行速度)不能为负数,因为我们不希望汽车回到起始线。

  • 奖励和回合:在穿越终点线之前的每一步奖励为 -1。当汽车驶出赛道时,它会被重置到起始单元格之一。回合 仅在 汽车成功穿越终点线时结束。

  • 起始状态:我们从起始线随机选择起始单元格;根据练习的描述,汽车的初始速度为 (0, 0)。

  • 零加速度挑战:作者提出了一个小的零加速度挑战,即在每个时间步骤中,以 0.1 的概率,动作将不起作用,汽车将保持之前的速度。我们可以在训练中实现这个挑战,而不是将其添加到环境中。

练习的解决方案分为两篇文章;在这篇文章中,我们将重点讨论如何构建赛道环境。此练习的文件结构如下:

|-- race_track_env
|  |-- maps
|  |  |-- build_tracks.py // this file is used to generate track maps
|  |  |-- track_a.npy // track a data
|  |  |-- track_b.npy // track b data
|  |-- race_track.py // race track environment
|-- exercise_5_12_racetrack.py // the solution to this exercise

此实现中使用的库如下:

python==3.9.16
numpy==1.24.3
matplotlib==3.7.1
pygame==2.5.0

构建赛道地图

我们可以将赛道地图表示为 2D 矩阵,不同的值表示赛道状态。我希望忠于练习,所以我正尝试通过手动分配矩阵值来构建书中展示的相同地图。这些地图将作为单独的.npy文件保存,以便环境在训练时可以读取文件,而不是在运行时生成它们。

这两个地图如下所示;浅色单元格是碎石,深色单元格是赛道表面,绿色单元格表示终点线,浅蓝色单元格表示起始线。

图 1 2D 矩阵表示的赛道地图。来源:作者提供的图像

构建类似 Gym 的环境

赛道地图准备好后,我们将创建一个类似于 gym 的环境,算法可以与之互动。 gymnasium,之前是 OpenAI gym,现在属于Farama Foundation,提供了一个用于测试 RL 算法的简单接口;我们将使用gymnasium包来创建赛道环境。我们的环境将包括以下组件/功能:

  • env.nS:观察空间的形状,在这个例子中为(num_rows, num_cols, row_speed, col_speed)。行数和列数在地图之间有所不同,但速度空间在所有赛道中是一致的。对于行速度,由于我们不希望汽车回到起点,行速度的观察值为[-4, -3, -2, -1, 0](负值因为我们期望汽车在地图上向上移动),总共有五个空间;列速度没有这样的限制,所以观察值范围从-4 到 4,总共有九个空间。因此,赛道示例中的观察空间形状为(num_rows, num_cols, 5, 9)。

  • env.nA:可能的行动数量。在我们的实现中有 9 种可能的行动;我们还将在环境类中创建一个字典,将整数行动映射到(row_speed, col_speed)元组表示,以控制智能体。

  • env.reset():当回合结束或汽车驶出赛道时,该函数将汽车带回起始单元格之一;

  • env.step(action):步进函数使算法通过采取行动与环境进行交互,并返回一个包含(next_state, reward, is_terminated, is_truncated)的元组。

  • 状态检查函数:有两个私有函数帮助检查汽车是否离开赛道或越过终点线;

  • 渲染函数:在我看来,渲染函数对定制环境至关重要;我总是通过渲染游戏空间和智能体的行为来检查环境是否已正确构建,这比单纯查看日志信息更直接。

牢记这些特点后,我们可以开始构建赛车道环境。

检查环境

到目前为止,所有赛车道环境所需的准备工作都已就绪,我们可以测试/验证我们的环境设置。

首先,我们渲染游戏以检查每个组件是否正常工作:

图 2 采用随机策略在两个轨道上行驶的智能体。来源:作者制作的动图

然后我们关闭渲染选项,让环境在后台运行,以检查轨迹是否正确终止:

// Track a
|   Observation   | reward | Terminated | Total reward |
|  (1, 16, 0, 3)  |   -1   |    True    |    -4984     |
|  (2, 16, 0, 2)  |   -1   |    True    |    -23376    |
|  (3, 16, 0, 3)  |   -1   |    True    |    -14101    |
|  (1, 16, 0, 3)  |   -1   |    True    |    -46467    |

// Track b
|   Observation    | reward | Terminated | Total reward |
|  (1, 31, -2, 2)  |   -1   |    True    |    -3567     |
|  (0, 31, -4, 4)  |   -1   |    True    |    -682      |
|  (2, 31, -2, 1)  |   -1   |    True    |    -1387     |
|  (3, 31, -1, 3)  |   -1   |    True    |    -2336     |

离策略蒙特卡罗控制算法

在环境准备好后,我们可以准备实施带加权重要性采样的离策略 MC 控制算法,如下所示:

图 3 离策略蒙特卡罗控制算法。来源:latex 由作者编写。

蒙特卡罗方法通过对样本回报进行平均来解决强化学习问题。在训练中,该方法基于给定策略生成轨迹,并从每个回合的尾部学习。在线策略和离策略学习之间的区别在于,在线策略方法使用一种策略来做决策和改进,而离策略方法使用不同的策略来生成数据和策略改进。根据书籍作者的说法,几乎所有的离策略方法都利用重要性采样来估计在一个分布下的期望值,给定来自另一个分布的样本。[2]

接下来的几个部分将解释上面算法中出现的软策略和加权重要性采样技巧,以及适当的值初始化对该任务的关键性。

目标策略和行为策略

本算法的离策略方法使用了两种策略:

  • 目标策略:正在学习的策略。在本算法中,目标策略是贪婪的和确定性的,这意味着在时间t,贪婪动作A的概率是 1.0,其它所有动作的概率为 0.0。目标策略遵循每步改进的广义策略迭代(GPI)。

  • 行为策略:用于生成行为的策略。本算法中的行为策略被定义为软策略,意味着在给定状态下所有动作都有非零概率。我们将在这里使用ε-贪婪策略作为我们的行为策略。

在软策略中,某个动作的概率是:

我们在为重要性采样生成动作时也应返回这个概率。行为策略的代码如下所示:

加权重要性采样

我们估计由目标策略生成的轨迹在行为策略下的相对概率,这样的概率方程是:

加权重要性采样的值函数是:

我们可以将方程重新调整,以适应增量更新形式的情景任务:

有很多优秀的例子展示了如何推导上述方程,因此我们在这里不再推导。但我也想解释算法最后几行的有趣技巧:

图 4 离策略蒙特卡罗控制算法中的技巧。来源:作者提供的图片

这个技巧基于目标策略在这里是确定性的设置。如果某个时间步采取的动作与目标策略的动作不同,那么该动作相对于目标策略的概率为 0.0;因此,所有后续步骤将不再更新轨迹的状态-动作值函数。相反,如果那个时间步的动作与目标策略相同,则概率为 1.0,动作值更新继续进行。

权重初始化

合适的权重初始化在成功解决此练习中发挥着重要作用。我们首先来看随机策略在两个轨道上的奖励:

// Track a
|   Observation   | reward | Terminated | Total reward |
|  (1, 16, 0, 3)  |   -1   |    True    |    -4984     |
|  (2, 16, 0, 2)  |   -1   |    True    |    -23376    |
|  (3, 16, 0, 3)  |   -1   |    True    |    -14101    |
|  (1, 16, 0, 3)  |   -1   |    True    |    -46467    |

// Track b
|   Observation    | reward | Terminated | Total reward |
|  (1, 31, -2, 2)  |   -1   |    True    |     -3567    |
|  (0, 31, -4, 4)  |   -1   |    True    |     -682     |
|  (2, 31, -2, 1)  |   -1   |    True    |     -1387    |
|  (3, 31, -1, 3)  |   -1   |    True    |     -2336    |

在穿越终点线之前,每一步的奖励是-1。训练的早期阶段,奖励将是较大的负值;如果我们将状态-动作值初始化为 0 或从均值为 0、方差为 1 的正态分布中随机值,则该值可能被认为过于乐观,这可能需要很长时间程序才能找到最佳策略。

通过几次测试,我发现均值为-500、方差为 1 的正态分布可能是更快收敛的有效值;你可以尝试其他值,绝对能找到比这更好的初始值。

解决问题

牢记上述所有思想,我们可以编写离策略蒙特卡罗控制函数,并用它来解决赛道练习。我们还将添加练习描述中提到的零加速度挑战。

然后我们在两个轨道上训练政策 1,000,000 个回合,包含/不包含零加速度挑战,并用以下代码进行评估:

通过绘制训练记录,我们发现策略在不考虑零加速度的情况下在两个轨道上都在第 10,000 集左右收敛;添加零加速度使得收敛变得更慢,并且在第 100,000 集之前不稳定,但之后仍然收敛。

图 5 轨道 A 的训练奖励历史。来源:作者提供的图像

图 6 轨道 B 的训练奖励历史。来源:作者提供的图像

结果

最后,我们可以让智能体使用训练后的策略进行游戏,同时绘制出可能的轨迹以进一步检查策略的正确性。

图 7 智能体基于训练后的策略在两个轨道上行驶。来源:作者提供的 GIF

轨道 a 的样本轨迹:

图 8 轨道 A 的样本最优轨迹。来源:作者提供的图像

轨道 b 的样本轨迹:

图 9 轨道 B 的样本最优轨迹。来源:作者提供的图像

到目前为止,我们已经解决了赛道练习。这种实现可能仍然存在一些问题,非常欢迎您指出这些问题并在评论中讨论更好的解决方案。感谢阅读!

参考

[1] Midjourney 服务条款: docs.midjourney.com/docs/terms-of-service

[2] Sutton, Richard S., 和 Andrew G. Barto. 强化学习:导论。麻省理工学院出版社,2018 年。

本练习的 GitHub 仓库: [链接];请查看“练习部分”。

使用 Q 学习解决出租车环境——教程

原文:towardsdatascience.com/solving-the-taxi-environment-with-q-learning-a-tutorial-c76c22fc5d8f

在动画 Jupyter Notebook 中实现 Q 学习,以解决 OpenAI Gym 的 Taxi-v3 环境

Wouter van Heeswijk, PhDTowards Data Science Wouter van Heeswijk, PhD

·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 3 月 20 日

--

图片由 Alexander Redl 提供,来源于 Unsplash

OpenAI Gym 中出租车环境的目标——是的,来自于 ChatGPT 背后的公司 和 Dall⋅E——是简单直接的,为强化学习(RL)领域提供了一个很好的入门介绍。

本文提供了一个逐步指南,介绍如何实现环境、使用表格 Q 学习学习策略,并通过动画可视化学习到的行为。如果你想在 RL 中迈出第一步,这个教程可能正适合你。

问题设置

在出租车环境中,一辆出租车需要接载乘客并将他们送到一个小停车场的目的地,尽可能地走最短路径。使用强化学习(RL)学习这样一个任务有多难呢?让我们来看看。

在深入实现之前,最好先将设置表述为 马尔可夫决策过程(MDP),以掌握问题的数学结构。

状态空间

一个 MDP 状态包含以下信息:(i)确定动作,(ii)计算动作奖励,(iii)计算过渡到下一个状态。

最明显的状态属性是出租车的位置;5*5 的网格给我们 25 个选项。此外,等待接送的乘客可以在 1 或 4 个点(标记为 Y、R、G、B)等待,或者他们可以在出租车内,总共提供了(4+1)个选项。最后,目的地也可以是 Y、R、G 或 B。我们用以下向量表示状态:

State = [x_pos_taxi, y_pos_taxi, pos_passenger, dest_passenger]

尽管这是一个非常简单的问题设置,但我们已经处理了 55(4+1)*4=500 个状态!将问题规模扩大到数千个位置,就很容易看出 MDP 的规模如何迅速膨胀(所谓的维度诅咒)。

可达的状态数量稍微小于 500,例如,乘客不会有相同的接送点和目的地。由于建模复杂性,我们通常关注完整的状态空间,代表最坏情况。

动作空间

状态空间可能比你一开始预期的要大,但动作空间仅包含六个决策:

  • 接送乘客

  • 接送乘客

如果动作不被允许(例如,尝试穿越墙壁,或在当前地点没有客户时尝试接客),代理将保持不动。

action_mask(在env.action_mask(state)中找到)告诉我们哪些动作是可行的,但这个可行性检查并不会自动强制执行。

奖励函数

在这个环境中可以区分三种类型的奖励:

  • 移动:-1。 每次移动都会带来小惩罚,以鼓励从起点到终点沿最短路径行驶。

  • 失败的接送:-10。 当乘客被放置在错误的位置时,自然会感到不满,因此应该给予较大的惩罚。

  • 成功的接送:20。 为了鼓励期望的行为,给予较大的奖励。

设计奖励的方式以鼓励期望的行为称为奖励塑形。例如,我们可以在成功接送后简单地结束游戏而不给予奖励(从而结束成本流),但明确的奖励信号通常大大简化学习。

转移函数

与大多数现实环境不同,出租车环境没有随机性。这个特性大大简化了学习,因为下一个状态完全由我们的动作决定。如果选择了不可行的动作——比如尝试穿越墙壁或接一个不存在的乘客——代理将会停在原地。

从抽象的角度来看,我们可以如下表示转移函数:

new_state = f(state,action)

明确的定义会考虑所有网格移动,(不)可行的接送和(不)可行的接客,这并不是特别困难,但需要一些专注。

游戏在成功将乘客送到目的地时结束,表示终止状态。

初始化

在数学上定义了问题(尽管不是非常详细)之后,我们将转向实现。

首先,我们安装必要的库,并随后导入它们。显然,我们需要安装gym环境。除此之外,我们只需要一些视觉相关的内容和你典型的数据科学库。

假设所有导入都正确,现在是时候进行一下基本检查了。让我们创建并渲染一个 Taxi-v3 环境。

我们应该看到一个介于 0 到 5 之间的动作,一个介于 0 到 499 之间的状态,以及一个阻止不可行动作的动作掩码。最重要的是,我们应该渲染一个静态画面,以确认我们确实已经启动并运行!

初始化输出的截图[图片由作者提供,使用OpenAI Gym]

最后,让我们设置两个函数:一个用于控制台输出和动画,另一个用于存储 GIF。请注意,动画只是 RGB 帧的重复绘图。它也非常耗时,因此不建议在训练期间运行。

测试随机代理

在确认环境按预期工作后,现在是时候让一个随机代理“肆意行动”了。正如你所猜测的,这个代理在每一个时刻都会采取随机行动。这本质上就是学习代理开始时的状况,因为它没有可靠的 Q 值可以使用。

你会看到代理尝试穿墙、在荒凉的地方接乘客,或者在错误的地点放下乘客。在极少数情况下,代理可能需要几千个动作——纯粹是偶然——才会将乘客放到正确的位置。

正如你可以想象的那样,随机代理让乘客感到无比沮丧:

未训练代理的动画。出租车在每个时间步选择随机动作,包括不可行的动作,直到最终偶然将乘客放到正确的站点[图片由作者提供,使用OpenAI Gym]

为什么要观看这么长的动画?好吧,这真的能给你一个未训练的 RL 代理是如何行为的印象,以及获取有意义的奖励信号需要多长时间。

注意:通过应用动作掩码——一个布尔向量,它实质上将不可行的动作的选择概率分配为 0——我们将自己限制在仅有的合理动作中,通常会大大缩短剧集的长度。

训练代理

现在是学习一些有用的东西的时候了。

我们将部署一个基础 Q 学习算法,采用ϵ-贪婪探索,选择ϵ概率的随机动作,其余时间使用 Q 值来选择动作。

Q 值在观察后使用以下公式更新。请注意,随着 500 个状态和 6 个动作的增加,我们必须填充一个大小为 500*6=3000 的 Q 表,每个状态-动作对需要多个观察来以任何程度的准确性学习其值。

在 Q 学习中,我们在每次观察后更新状态-动作对的值,使用脱离策略的学习。

代码如下。你可以调整超参数;根据我的经验,这个问题对它们的敏感性较低。

训练的收敛情况如下图所示。经过 2000 个回合后,我们似乎学会了一个相当好的、稳定的策略。

起初,代理需要许多步骤才能成功完成一个回合。最终,正向奖励会被识别,代理开始采取越来越高效的动作,最终实现最短路径解决方案 [作者图片]

注意:代码忽略了动作掩码,并在出现平局时选择第一个 Q 值。虽然提供了两种问题的替代方案,但它们大大增加了每个回合的计算工作量,并且在此设置下没有明显的性能提升。

测试策略

让我们看看我们学到了什么。根据我们所在的状态,我们在 Q 表中查找相应的 Q 值(即每个状态的六个值,对应于动作),并选择 Q 值最高的动作。

实质上,Q 值捕捉了与动作相关的预期累计奖励。注意,我们不再以概率ϵ选择随机动作——这一机制仅用于学习。

如果你进行了足够多的迭代,你应该会看到出租车总是直接驶向乘客,选择最短路径到达目的地,并成功接送乘客。的确,自从随意驾驶以来,我们已经取得了长足的进步。

成功学习到的策略动画。出租车将沿着最短路径接送乘客,最大化奖励 [作者图片,使用 OpenAI Gym]

尽管在一个看似简单的环境中需要大量观察来学习这一策略,但如果你意识到算法在完全不了解其操作环境的情况下学会了这些,你可能会更欣赏这个结果!

总结

Taxi 环境是一个很好的强化学习入门选择。问题设置简单直观,但可以很容易地扩展到更现实的场景(例如现实的城市网络、管理出租车队、不可预测的旅行时间)。它在计算上易于管理,但也展示了即使在微不足道的问题设置下学习策略所需的计算工作量。

如果你是强化学习的新手,强烈建议你在设计和解决自己的问题之前,先在 OpenAI Gym 环境中进行尝试。

完整的 Jupyter Notebook 可以在我的 GitHub 上找到:

github.com/woutervanheeswijk/taxi_environment

Lexi Anderson 拍摄,来源于 Unsplash

进一步阅读

Q-learning 和 SARSA 简介:

## 使用非策略强化学习的悬崖行走

对非策略和策略强化学习的深入比较

towardsdatascience.com

构建马尔可夫决策过程:

## 马尔可夫决策过程的五大构建块

通过掌握马尔可夫决策的基础原则来定义和沟通你的强化学习模型……

towardsdatascience.com

实现 Deep Q-learning 的教程:

## TensorFlow 2.0 中的深度 Q 学习的最小工作示例

多臂老丨虎丨机示例用于训练 Q 网络。更新过程使用 TensorFlow 仅需几行代码

towardsdatascience.com

参考文献

本文中详细介绍的笔记本部分基于以下来源并改编了代码:

[1] OpenAI Gym. Taxi-v3 环境。OpenAI Gym 环境在MIT 许可证下提供。

[2] LearnDataSci. 从头开始在 Python 中实现强化 Q-learning 与 OpenAI Gym。Taxi-v2 实现。

[3] Botforge. 保存 OpenAI Gym 渲染为 GIFS。公开 GitHub Gist。

使用深度强化学习解决 Unity 环境

原文:towardsdatascience.com/solving-unity-environment-with-deep-reinforcement-learning-836dc181ee3b?source=collection_archive---------10-----------------------#2023-02-20

深度强化学习代理的 PyTorch 实现的端到端项目

Gabriel CassimiroTowards Data Science Gabriel Cassimiro

·

查看 发表在 Towards Data Science ·6 min read·2023 年 2 月 20 日

--

图片由 Arseny Togulev 提供,来源于 Unsplash

Unity 是一个流行的游戏开发引擎,允许开发者创建具有惊人图形和沉浸式游戏玩法的游戏。它被广泛用于开发各种平台上的游戏,包括移动设备、PC 和控制台。然而,创建智能且具有挑战性的游戏环境对游戏开发者来说是一项艰巨的任务。这就是深度强化学习(DRL)的作用所在。

DRL 是一种机器学习的子集,结合了深度学习和强化学习。它是一种强大的技术,已被用于解决各种领域中的复杂任务,包括机器人技术、金融和游戏。近年来,DRL 已成为构建智能游戏代理的流行方法,这些代理可以从经验中学习并适应新情况。

在这篇文章中,我们将探讨如何使用 DRL 解决 Unity 游戏环境中的问题。我们将通过在 Unity 环境中实现 DRL 来收集香蕉。我们还将探讨使用 DRL 进行游戏开发的一些挑战,以及如何克服这些挑战。

这是 Udacity 深度强化学习专业课程中的一个项目。完整项目和代码可以在这个Github 仓库中找到。

目标

该项目的目标是使用深度 Q 学习训练一个代理。代理将被训练以收集黄色香蕉,同时避免 Unity 的香蕉收集器环境中的蓝色香蕉。

有关 Unix 环境的更多信息可以在这里找到。代理使用深度 Q 学习算法进行了训练,并在 775 个回合中成功解决了环境。

环境与任务

该环境由一个方形世界组成,其中有黄色和蓝色香蕉。代理的目标是尽可能多地收集黄色香蕉,同时避免蓝色香蕉。代理有 4 个可能的动作:向前移动、向后移动、左转和右转。

状态空间具有 37 维,包括代理的速度以及基于射线的周围对象感知。收集一个黄色香蕉会获得 +1 奖励,收集一个蓝色香蕉会获得 -1 奖励。

任务是阶段性的,为了解决环境,代理必须在 100 个连续的回合中获得平均 +13 的分数。

环境如下所示:

作者提供的图片

代理

为了解决环境所给出的问题,实现了一种深度 Q 学习算法。该算法基于 DeepMind 的论文通过深度强化学习实现人类水平的控制

该算法通过使用神经网络来近似 Q-函数。神经网络接收状态作为输入,并为每个动作输出 Q-值。然后它使用 Q-值来选择代理需要采取的最佳动作。该算法通过使用 Q-Learning 算法来训练神经网络。简单实现算法时还存在两个问题:相关经验和相关目标。该算法使用了两种技术来解决这些问题:经验回放和固定 Q-目标。

相关经验

相关经验是指代理的经验(或转移)彼此之间相关,即它们不是独立同分布的。这可能导致对特定状态或动作的期望奖励的高估,从而导致性能差或收敛到次优策略。

为了解决这个问题,使用了一种叫做 经验回放 的技术。该技术包括将代理的经验存储在回放缓冲区中,并从中随机抽样以训练神经网络。

相关目标

相关目标是指用于更新策略的目标值彼此之间不是独立的,导致学习信号的相关性。这可能会减慢或阻止收敛到最佳策略。

为了解决这个问题,使用了一种叫做 固定 Q-目标 的技术。该技术包括使用两个神经网络:本地网络和目标网络。本地网络用于选择代理需要采取的最佳动作,而目标网络用于计算 Q-Learning 算法的目标值。目标网络每 4 步更新一次,与本地网络的权重同步。

这是 Python 中的实现:

神经网络架构

算法中使用的神经网络架构是一个简单的全连接神经网络,具有 2 层隐藏层。输入层有 37 个神经元,输出层有 4 个神经元,隐藏层每层有 64 个神经元。隐藏层使用的激活函数是 ReLU,输出层使用的激活函数是恒等函数。

此实现中使用的优化器是 Adam,学习率为 0.0005。

实现神经网络所使用的库是 PyTorch。

这是神经网络的实现:

训练任务

为了训练代理,我们使用了一个循环与环境交互,收集并从经验中学习。训练任务中使用的超参数之一是回合数。第一个超参数是手动调整的,目的是优化训练时间和代理的性能。最终实现中使用的回合数为 1200,但环境在 775 回合中被解决。

训练任务中使用的第二个超参数是每集的步数。这个超参数也通过手动调整来优化训练时间和智能体的性能。步数越多,智能体可以越多地探索环境,但训练时间也会大幅增加。最终实现中每集的步数是 1000。

其他一些使用的超参数:

  • 回放缓冲区大小:1000

  • 批量大小:32

  • 更新频率:4

  • Gamma:0.99

  • Tau:1e-3

  • 学习率:0.0005

通过这些措施,我们能够在 775 集内解决环境问题。下面的图表显示了智能体在获得更高奖励方面的进展。

作者提供的图片

在这里,我们可以看到随着智能体的改进,奖励增加。在图中也可以看到探索与开发之间的权衡,智能体在前 200 集时更多地进行探索,然后开始开发环境并获得更高的奖励。

完整的实现可以在这个GitHub 仓库中找到。

尽管我们能够解决环境问题,但仍有一些改进可以应用,以加快解决速度。

未来改进

算法可以通过以下技术进行改进:

另一个可能的改进是使用环境中的像素数据。这些改进可能会成为新文章的主题,我打算深入探讨核心概念并用 TensorFlow 实现神经网络。

感谢阅读!

这里有一些你可能感兴趣的其他文章:

## 对象检测与 Tensorflow 模型和 OpenCV

使用训练好的模型识别静态图像和实时视频中的对象

## 如何为 GCP 专业机器学习工程师考试做好准备 ## 对象检测与 Tensorflow 模型和 OpenCV

课程评审、学习技巧以及我的实践经历

## 如何为 GCP 专业机器学习工程师考试做好准备

LLM 提示中的思维链:结构化 LLM 推理概述

原文:towardsdatascience.com/something-of-thought-in-llm-prompting-an-overview-of-structured-llm-reasoning-70302752b390?source=collection_archive---------1-----------------------#2023-09-16

思维链(CoT)、思维树(ToT)、思维图(GoT)以及其他相关概念,这些思维究竟是什么?

Yunzhe WangTowards Data Science Yunzhe Wang

·

关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 9 月 16 日

--

“思维树”,由 Midjourney 生成

在智能手机和智能家居的时代,想象一个不仅仅按照指令执行的 AI,而是像我们一样进行复杂逻辑思考。这听起来像是科幻小说,对吗?然而,如果你玩过 ChatGPT,你可能已经亲眼目睹了这种惊人的能力。即使是著名的 AI 推理专家 Hector Levesque 也曾对此感到惊讶,并曾对 AI 传奇人物 Geoffrey Hinton 评论道:“这种愚蠢的方法(指神经网络)怎么能进行推理?”

虽然这个故事突出了人工智能的巨大进步,但这些进步的真正精髓在于大型语言模型(LLMs)与推理的复杂互动。进入这种互动的入口是提示工程——优化提供给 LLM 的文本输入以引出期望输出的艺术与科学。从本质上讲,它涉及理解像 ChatGPT、Bard、Claude、LLama 等语言模型如何响应不同提示的复杂性,然后利用这些知识来实现特定结果。

把大型语言模型(LLMs)看作是庞大的知识库。你提问或陈述的方式(提示)决定了如何挖掘这个知识库。就像人类根据问题的表述可能会给出不同的答案一样,LLMs 也可能根据输入给出不同的回应。

在这篇文章中,你将获得关于各种提示工程框架的简要概述,这些框架旨在提升 LLM 推理能力,包括:

  • Chain-of-Thought

  • Chain-of-Thought-Self-Consistency

  • Tree-of-Thoughts

  • Graph-of-Thoughts

  • Algorithm-of-Thoughts

  • Skeleton-of-Thought

  • Program-of-Thoughts

Chain-of-Thought (CoT)

不是直接给出答案,而是为语言模型提供中间推理示例,以引导其回答。

Chain-of-Thought (CoT) 提示已被认可为开创性和最具影响力的提示工程技术之一,增强了大型语言模型的决策过程。不同于强调直接输入输出交互的传统提示方法,CoT 强制模型将其推理分解为中间步骤。这种方法与人类认知过程类似,其中复杂的挑战被分解为更小、更易管理的组成部分。

举个例子,考虑一个数学问题:“Roger 拥有 5 个网球,然后购买了 2 罐网球,每罐包含 3 个球。他现在拥有多少个网球?”。与其直接推导出答案是 11,一个人可能会推理:“最初,Roger 有 5 个球。2 罐球的总数,每罐包含 3 个球,总计 6 个球。将这些值相加,5 + 6,得到 11。”将这种逐步分析推理整合到输入提示中,不仅提高了模型响应的准确性,还可以在不需要额外训练数据集或改变基本模型配置的情况下完成。

思维链提示,来源: Wei et al. (2022)

思维链自一致性 (CoT-SC)

构建多个思维链,评估每一个链,最终选择最有效且连贯的链。

从思维链框架发展而来的一个进步是 CoT 自一致性。该方法在响应查询时发起多个并行的推理路径,并在最终确定答案之前应用加权机制。这种方法类似于传统机器学习中观察到的集成技术,但它被应用于大语言模型中的思维序列。

思维树 (ToT)

扩展思维链以树形结构呈现。这允许回溯,探索从一个根思想衍生出的多个推理分支。

思维树 (ToT) 提供了一个更结构化的 LLM 推理框架,通过将复杂问题分解为更可管理的部分来进行思考。与通过链条链接进行推理的 CoT 不同,ToT 将其问题解决策略组织为树形结构。每个节点,被称为“思维”,是一个连贯的语言序列,作为通向最终答案的步骤。通过将问题分解为这些离散的“思维”单元——从填字游戏中的简短词组到数学方程的一个组件——ToT 确保问题的每个阶段都得到系统性地解决。

ToT 在 24 点游戏中的推理,来源: Yao et al. (2023)

ToT 的操作优势在于其系统化的组织。首先,系统将问题分解,并从当前状态生成一系列潜在的推理步骤或“思维”候选。然后对这些思维进行评估,系统会评估每个思维引导到期望解决方案的可能性。标准搜索算法,如宽度优先搜索(BFS)和深度优先搜索(DFS),用于遍历这棵树,帮助模型识别最有效的思维序列。

ToT 的重要性源于其整体设计、适应性和效率。链式思维提示可以被视为 ToT 框架中的一个具体实例。其模块化特性表明,从问题的初步分解到所使用的搜索算法,单独的组件可以独立运行。

思维图谱(GoT)

将树结构演变为有向无环图。这引入了自环,可以巩固特定的思路或将多个思路汇聚成一个连贯的想法。

思维图谱 (GoT) 框架代表了从 CoT 和 ToT 方法的先进进展。GoT 框架的核心是将想法概念化为有向无环图(DAG)中的顶点。在这种情况下,每个顶点对应于一个特定的思路或解决方案——无论是初步的、中间的还是最终的——由输入刺激引发。图中的有向边描绘了这些思路之间的相互依赖性。具体来说,如果一条边从思路 t1 延伸到 t2,则表示 t2 是基于 t1 形成的。这种系统化允许多种思路,因为节点可以被分类为“计划”或“结果”等不同类别。

思维图谱,来源:Besta 等 (2023)

GoT 的新颖性在于其对这些思路应用变换,从而进一步精炼推理过程。主要的变换包括聚合,允许将几个思路融合为一个统一的想法;精炼,通过对单一思路进行不断迭代以提高其准确性;以及生成,促进从现有思路中产生新想法。这些变换,强调推理路径的融合,相较于以前的模型如 CoT 或 ToT,提供了更复杂的观点。

此外,GoT 通过评分和排名引入了评估维度。每个个体思路,由一个顶点表示,都根据其相关性和质量进行评估,由指定的评分函数辅助。重要的是,这个函数考虑了整个推理链,分配的分数可能会与图中的其他顶点相关联。该框架还为系统提供了根据各自分数对这些思路进行分层的能力,这一特性在判断哪些想法值得优先考虑或实施时非常有用。

思维算法(AoT)

保持一个不断发展的上下文链,消除了像思维树那样的冗余查询。它探索了一条可变的推理路径。

尽管 ToT 和 GoT 通过基于搜索的机制解决 LLM 推理挑战,产生大量图形形式的推理路径。然而,它们对众多 LLM 查询的高度依赖,有时对于单个问题需要数百次查询,导致计算效率低下。

思维算法(AoT)提供了一种创新的方法,具有动态和可变的推理路径。通过维持单一的不断演变的思想上下文链,AoT 整合了思想探索,提高了效率,减少了计算开销。

思维算法。每个框表示一个独特的思想。绿色代表有前景的思想,红色代表较少有前景的思想。注意:ToT 有多个查询,而 AoT 保持单一上下文,来源:Sel 等(2023)

AoT 的独创性来自于观察到 LLM 尽管强大,但在面对新而熟悉的问题时,有时会回到之前的解决方案。为了克服这一点,AoT 吸收了上下文示例,借鉴了深度优先搜索(DFS)和广度优先搜索(BFS)等经过时间考验的搜索算法。通过模拟算法行为,AoT 强调了实现成功结果和从失败尝试中汲取见解的重要性。

AoT 的基石在于其四个主要组成部分:1) 将复杂问题分解为易于处理的子问题,考虑它们之间的相互关系以及单独处理的难易程度;2) 对这些子问题提出连贯的解决方案,保持连续和不间断的方式;3) 直观地评估每个解决方案或子问题的可行性,无需依赖明确的外部提示;4) 根据上下文示例和算法指南,确定最有前景的探索路径或回溯路径。

思维骨架(SoT)

首先生成答案蓝图,然后平行展开细节,从而减少生成完整响应的时间。

思维骨架(SoT)范式独特地设计,并非主要为了增强大型语言模型(LLM)的推理能力,而是解决最关键的挑战——最小化端到端生成延迟。该方法基于双阶段方法,首先生成初步答案蓝图,然后进行全面扩展。

思维骨架,来源:宁等(2023)

在初始的“骨架阶段”,模型不是生成一个全面的响应,而是生成一个简洁的答案骨架。这个通过精心设计的骨架模板生成的简略表示,捕捉了潜在答案的核心要素,从而为后续阶段奠定基础。

在随后的“扩展点阶段”,LLM 系统地扩展答案骨架中每个组件。利用扩展点提示模板,模型同时对骨架的每个部分进行详细说明。这种将生成过程分为初步骨架制定和并行详细扩展的二分法方法,不仅加速了响应生成,还努力保持输出的连贯性和精确性。

思维程序(PoT)

将问题回答的推理形成一个可执行程序,将程序解释器的输出作为最终答案的一部分。

思维程序(PoT)是一种独特的 LLM 推理方法,它不仅仅是生成自然语言的答案,PoT 要求创建一个可执行程序,这意味着它可以在程序解释器(如 Python)上运行,从而产生实际结果。这种方法与更直接的模型形成对比,强调其将推理分解为顺序步骤并将语义与变量关联的能力。因此,PoT 提供了一个更清晰、更具表现力且有实据的答案生成模型,提高了准确性和理解,特别适用于需要数字计算的数学类型逻辑问题。

需要注意的是,PoT 的程序执行不一定以最终答案为目标,而可能是最终答案的一个中间步骤。

CoT 与 PoT 的比较,来源:Chen et al. (2022)

在不断发展的 AI 领域中,结构化推理框架如链式思维(Chain-of-Thought)极大地改变了我们对大型语言模型(LLM)能力的认知和利用方式。它们象征着一种转变,向着不仅仅是信息重复的模型迈进,同时也进行复杂的推理,这与人类认知过程非常相似。展望未来,潜在的前景似乎无穷无尽。设想一个 AI,能够生成不仅准确的答案,还能提供强大、可编程的解决方案,或具备可视化其思维过程的能力,使 AI 与人类的合作更加无缝。这些进步建立在本文探索的基础框架上,预示着一个 LLM 成为问题解决、创造力和决策不可或缺的伴侣的未来,推动我们与技术的共生关系发生范式转变。

如何按多个列在 R 中对数据框进行排序

原文:towardsdatascience.com/sort-r-dataframes-7fe3b0a1fbbd

在 R 编程语言中对数据框进行排序

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发布于 Towards Data Science ·4 分钟阅读·2023 年 4 月 27 日

--

Andre Taissin 拍摄的照片,来源于 Unsplash

R 是一种被统计学家和机器学习科学家广泛使用的编程语言。数据框是强大的 R 构造体,能够有效且强大地进行数据处理、操作和分析。

在今天的简短教程中,我们将演示如何在一个或多个列上对 R 数据框进行排序,无论是降序还是升序。

首先,让我们在 R 中创建一个数据框,我们将在本教程中引用它,以演示一些有关排序的有用概念。

df <- data.frame(
  id = c(1:8),
  value = c(123, 86, 234, 235, 212, 121, 123, 899),
  colC = c("A", "C", "B", "B", "A", "B", "C", "C"),
  colD = c(TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE)
)

df
  id value colC  colD
1  1   123    A  TRUE
2  2    86    C  TRUE
3  3   234    B FALSE
4  4   235    B FALSE
5  5   212    A FALSE
6  6   121    B FALSE
7  7   123    C  TRUE
8  8   899    C FALSE

按单列对 R 数据框进行排序(升序或降序)

现在假设我们想按照特定列对数据框进行排序。为此,我们可以利用 [order()](https://stat.ethz.ch/R-manual/R-devel/library/base/html/order.html) 函数,并相应地切片数据框。在 order 函数中,我们可以指定在排序数据框时 R 需要考虑的列。

升序

现在假设我们想按照 value 列的升序对数据框进行排序。默认情况下,order() 函数将以升序对数据框进行排序。

df[with(df, order(value)), ]

  id value colC  colD
2  2    86    C  TRUE
6  6   121    B FALSE
1  1   123    A  TRUE
7  7   123    C  TRUE
5  5   212    A FALSE
3  3   234    B FALSE
4  4   235    B FALSE
8  8   899    C FALSE

降序

如果你想指定降序,只需在列值前加上负号,这样就会表示我们想要反转升序。

df[with(df, order(-value)), ]

  id value colC  colD
8  8   899    C FALSE
4  4   235    B FALSE
3  3   234    B FALSE
5  5   212    A FALSE
1  1   123    A  TRUE
7  7   123    C  TRUE
6  6   121    B FALSE
2  2    86    C  TRUE

按多个列对数据框进行排序

order() 函数可以接受任意数量的列名。这意味着我们可以指示语言对数据框进行多列排序。同时,也可以对不同的列使用不同的排序策略(即降序与升序)。

我们在 order() 函数中提供的列名顺序将决定该函数对输入数据框进行排序的实际顺序。

现在假设我们想要根据 valueid 列来排序我们的数据框,分别按降序和升序排序。我们可以使用以下代码片段来实现。

df[with(df, order(-value, id)), ]

  id value colC  colD
8  8   899    C FALSE
4  4   235    B FALSE
3  3   234    B FALSE
5  5   212    A FALSE
1  1   123    A  TRUE
7  7   123    C  TRUE
6  6   121    B FALSE
2  2    86    C  TRUE

注意 id=1id=7 的条目是如何按照升序排序的,这些条目是基于 value 列的降序排序的。

现在假设我们想要按照相同的列对数据框进行排序,不过这次我们将对两个列都使用降序排序。

df[with(df, order(-value, -id)), ]

  id value colC  colD
8  8   899    C FALSE
4  4   235    B FALSE
3  3   234    B FALSE
5  5   212    A FALSE
7  7   123    C  TRUE
1  1   123    A  TRUE
6  6   121    B FALSE
2  2    86    C  TRUE

如前所述,请确保提供列的顺序,以便函数在排序输入数据框时考虑这些列。注意,当 order() 函数的输入列发生变化时,输出结果将如何改变:

df[with(df, order(-id, -value)), ]

 id value colC  colD
8  8   899    C FALSE
7  7   123    C  TRUE
6  6   121    B FALSE
5  5   212    A FALSE
4  4   235    B FALSE
3  3   234    B FALSE
2  2    86    C  TRUE
1  1   123    A  TRUE

最终思考

在今天的简短教程中,我们演示了如何根据某些列对 R 中的数据框进行排序,无论是升序还是降序。数据框是强大的结构,让 R 程序员和统计学家能够分析数据并提取有意义的见解。

数据框排序是分析数据时最基本的方面之一。我希望这篇文章能帮助您了解如何根据特定要求快速排序数据框。

👉 成为会员 并阅读 Medium 上的每一个故事。您的会员费直接支持我和其他您阅读的作者。您还将获得对 Medium 上每个故事的完全访问权限。

gmyrianthous.medium.com/membership?source=post_page-----7fe3b0a1fbbd-------------------------------- [## 通过我的推荐链接加入 Medium — Giorgos Myrianthous

作为 Medium 会员,您的会员费的一部分将分配给您阅读的作者,并且您可以完全访问每一个故事……

gmyrianthous.medium.com

👇相关的文章你可能也喜欢 👇

gmyrianthous.medium.com/membership?source=post_page-----7fe3b0a1fbbd-------------------------------- ## ETL 与 ELT:有什么区别?

关于数据工程中 ETL 和 ELT 的比较

[towardsdatascience.com ## 创建本地 dbt 项目

如何使用 Docker 创建一个包含虚拟数据的本地 dbt 项目用于测试

[towardsdatascience.com

使用 Typescript 进行空间数据工程

原文:towardsdatascience.com/spatial-data-engineering-with-typescript-fb5f59af8bb0

T KUnsplash 上的照片

建立自动化空间数据科学的数据管道

Sutan MuftiTowards Data Science Sutan Mufti

·发表于 Towards Data Science ·9 分钟阅读·2023 年 9 月 5 日

--

介绍

我们可以把数据看作水,把公司看作城镇。就像一个城镇随着人口增长而需要更多的水来服务其居民一样,一家公司在成长时也需要现成的数据来支持其运营。这类公司需要一个数据管道系统,类似于将水送到城镇家庭的管道和基础设施。在我们的数据类比中,数据工程师就是那些建立和维护这些数据管道的人。对于常规的数组或表格数据来说,这很简单,但对于空间数据来说,就会复杂一些。

空间数据与常规数据略有不同;它包含空间属性。这些属性使我们能够建立空间关系,也称为 地理空间拓扑。即使两个表没有主键和外键,只要它们都有空间属性,我们仍然可以将它们连接起来。如果我们可视化空间属性,就会得到一张地图!

## 空间数据科学:SQL 空间连接

如果表格之间有空间关系,则将它们连接起来。附加说明:我正在使用 Ms. Excel 进行操作

towardsdatascience.com

构建空间数据管道与创建普通数据管道有所不同。在这种情况下,我们主要使用空间 SQL 处理空间数据属性,这是数据科学家社区中较少见的技能。一旦数据管道建立,数据分析师可以继续分析来自数据管道的空间数据。这意味着数据分析师可以专注于生成空间洞察,而无需担心数据的可用性。这也意味着可以使用最新的空间数据自动生成地图。

本文

本文讨论了如何使用 TypeScript 和 NodeJS 构建空间数据管道。我们可以将其称为ETL(提取、转换、加载)过程,但处理的是空间数据。首先,我们将演示如何使用 TypeScript 从源头获取数据(提取)。接下来,我们将把这些数据转换成适合存储的结构(转换)。最后,我们将操作空间 SQL 来管理和存储数据到我们的数据库中(加载)。

开发使用 TypeScript 和 Node.js 完成,主要使用了node-postgres库。演示代码可在以下链接中找到。

[## GitHub - sutanmufti/spatial-data-engineering-typescript: 使用 TypeScript 进行空间数据工程…

使用 TypeScript 和 Node.js 进行空间数据工程。贡献者为 sutanmufti/spatial-data-engineering-typescript…

github.com](https://github.com/sutanmufti/spatial-data-engineering-typescript?source=post_page-----fb5f59af8bb0--------------------------------)

先决条件

有三个先决条件:你需要有 Node.JS 和 TypeScript,以及一个按需的 PostGIS 服务器。我在这篇文章中使用的是 Mac OS(UNIX),但这篇文章适用于任何 Linux 或类似 UNIX 的服务器操作系统。

Node.JS

Node.js 是一个在服务器上执行 JavaScript 代码的 JavaScript 运行时。它的功能类似于 Python,充当 JavaScript 代码的解释器。对于本文,基本理解 JavaScript 语法即可。

[## 下载 | Node.js

Node.js® 是一个基于 Chrome 的 V8 JavaScript 引擎构建的 JavaScript 运行时。

nodejs.org](https://nodejs.org/en/download?source=post_page-----fb5f59af8bb0--------------------------------)

TypeScript

TypeScript 是 JavaScript 的超集,为 JavaScript 添加了类型检查功能。这种“类型检查功能”限制了 JavaScript 编码,从而降低了出错的风险。我们在 TypeScript 中开发的代码会被编译成 JavaScript 代码,然后由 Node.JS 执行。虽然 TypeScript 是可选的,但我喜欢在我的项目中使用类型检查。

[## 如何设置 TypeScript

将 TypeScript 添加到你的项目中,或者全局安装 TypeScript

TypeScript 官方网站

Postgis 服务器

你必须运行 PostgreSQL 服务器,并安装 Postgis。Postgis 是一个 PostgreSQL 扩展,允许我们处理和存储地理空间数据。这就是我们如何进行空间 SQL 查询的方式。当然,你必须了解基本的 SQL 才能操作 PostgreSQL。

[## 空间数据科学:空间查询

用 SQL 回答每一个“在哪里”的问题;带有示例

Towards Data Science

你可以通过 Docker 拥有自己的 Postgis 服务器,或者直接在你的 PC 上安装它。另一种选择是部署一个云实例,例如使用 Amazon RDS、Google Cloud SQL,或一个安装了 PostgreSQL 的普通虚拟机。

[## Docker

编辑描述

Docker Hub 上的 Postgis 镜像

理念

我们可以将这项工作分解为几个任务:

  • 提取:从数据源获取数据。对于本文,数据源可以是任何东西,只要我们接收的是 GeoJSON 格式 (RFC7946)!在这个演示中,我们将采用 Stuart Grange 的数据,该数据源自伦敦政府的 数据存储。我们也可以检索其他地理空间格式如 shapefile,但那意味着我们必须使用其他库来处理。为了简便起见,我们就使用 GeoJSON。

  • 转换:验证数据(再次说明,这不是本文的重点)并处理无效数据。然后,我们将原始数据转换成数据分析师使用的结构;或者说我们如何设计我们的数据环境。

  • 加载:将数据插入到表中。这使用空间 SQL。

在文章的最后,我们可以看到我们的数据存储在 Postgis 服务器中。让我们进入代码部分。

构建数据管道

主要代码可以在以下链接中找到。让我们分解任务并逐步分析代码。

GitHub - sutanmufti/spatial-data-engineering-typescript: 使用 TypeScript 进行空间数据工程…

使用 TypeScript 和 Node 进行空间数据工程。贡献于 sutanmufti/spatial-data-engineering-typescript…

GitHub - sutanmufti/spatial-data-engineering-typescript

主要功能是以下代码。如你所见,它主要包含了 2 个函数:ExtractDatatransformAndLoad。此外,还有一个名为pool的常量变量,用于处理 PostGIS 服务器的认证。

async function main(){
    // this creates the pool connection to the postgis server
    const pool = new Pool({
        user: process.env.POSTGRES_USER,
        host: process.env.POSTGRES_HOST,
        database: process.env.POSTGRES_DB,
        password: process.env.POSTGRES_PASSWORD,
        port: Number(process.env.POSTGRES_PORT),
      });

    // this fetches the data
    const data = await ExtractData()
    // this transforms and load the data based on the Pool connection
    await transformAndLoad(data.features,pool)

}

提取数据

这个任务是由ExtractData()函数完成的。我们期望这个函数能返回类似 GeoJSON 的数据。

// located in /src/lib/functions.ts

export async function ExtractData() {
    // link used in demo is https://skgrange.github.io/www/data/london_low_emission_zones.json
    const res = await fetch('https://<linktodata>/')
    const data = <GeoData> await res.json()
    // add validation function to handle error here.
    return data
}

在这个函数中,我们有一个名为ExtractData的异步函数。export语句表明这个函数可以在另一个 TypeScript 文件中导入,这使得项目模块化。async语法表示这是一个异步函数。这使我们可以同时运行多个函数,主要因为我们将使用 fetch API 来执行 HTTP GET 请求。

Fetch API 是一个异步函数,更容易嵌套在异步函数中。因此,我们有await语句来等待 HTTP Get 请求完成。我们将以GeoJSON格式获得data

我发现 fetch API 和异步函数对初学者来说比较混乱。此外,异步函数如何与 Fetch API 互补的内容在我的另一篇文章中有介绍。

[## 使用 Fetch API

如何在 Javascript 中使用 Fetch?为什么 Fetch 返回一个 Promise?介绍异步函数和 Promises

insight.sutan.co.uk](https://insight.sutan.co.uk/using-fetch-api-faa44d4d9a12?source=post_page-----fb5f59af8bb0--------------------------------)

转换和加载数据

在获取数据后,我们可以将其转换并加载到 PostGIS 中。我们将使用node-postgres作为客户端来用 nodeJS 执行 SQL。我偏爱node-postgres因为我喜欢编写原始 SQL 代码。此外,我们对空间 SQL 有更细粒度的控制。缺点是我们的代码可能会显得复杂,因为我们可能会编写多行 SQL 查询。

[## node-postgres

node-postgres 是一个用于与 PostgreSQL 数据库接口的 node.js 模块集合。

node-postgres.com](https://node-postgres.com/?source=post_page-----fb5f59af8bb0--------------------------------)

这部分稍微长一点。我将突出重点,主要是空间 SQL 部分。

// This function transforms and loads the data into postgis
export async function transformAndLoad(geojsonFeatures: Feature[], pool: pg.Pool) {
    const client = await pool.connect();

    try {
      await client.query('BEGIN'); // Start a new transaction

      for (const geojsonPolygon of geojsonFeatures) {
        const geometryType = geojsonPolygon.geometry.type;
        const coordinates = geojsonPolygon.geometry.coordinates;

        const insertQuery = `
          INSERT INTO public.data (geometry, name, type,id)
          VALUES (ST_setsrid(ST_GeomFromGeoJSON($1),4326), $2, $3,$4)
        `;

        const values = [
          JSON.stringify({ type: geometryType, coordinates }),
          geojsonPolygon.properties.name,    
          geojsonPolygon.properties.type,    
          getISOstring()
        ];

        await client.query(insertQuery, values); // Insert the GeoJSON data
      }

      await client.query('COMMIT'); // Commit the transaction if successful
        // await client.query('ROLLBACK');
        console.log('Bulk insert of POLYGON features successful');
    } catch (error) {
      await client.query('ROLLBACK'); // Roll back the transaction on error
      console.error('Bulk insert failed:', error);
    } finally {
      client.release(); // Release the database connection\
      pool.end()
    }
  }

使用 Node-postgres 进行安全数据事务

在 PostgreSQL 中插入批量行的基本代码如下:

await client.query('BEGIN');
try {
// ... start inserting data...
    await client.query('COMMIT');
} catch {
    // this code runs when we can't insert the data somehow. handle the error.
    // cancels the data transactions as a whole, avoiding duplicates.
    await client.query('ROLLBACK');
}

client.query("BEGIN") 允许我们为每条数据记录进行插入过程的分阶段处理。这意味着如果在插入过程中出现问题(例如,第 n 行的数据无效,导致 PostgreSQL 抛出错误),我们可以取消整个数据事务。这个取消操作通过 client.query('ROLLBACK') 完成。如果所有行都正确插入且没有错误,我们可以将其声明为安全插入,并调用 client.query("COMMIT") 来提交数据事务。

如果我们不以 client.query("BEGIN") 开始,那么在插入过程中发生的任何错误都会导致数据被插入到数据库中。在大多数情况下,我们不希望这样。

参数化查询

让我们看看在调用 client.query("COMMIT") 后我们会做什么。这是我们转换并插入空间数据的地方。转换的部分不多,因为我只是提取了一些属性。

// transformation
const geometryType = geojsonPolygon.geometry.type;
const coordinates = geojsonPolygon.geometry.coordinates;

最重要的一点是如何使用参数化查询。一个常见的 SQL 漏洞是 SQL 注入。参数化查询通过不将原始 SQL 插入到我们的数据事务中来缓解这个漏洞。看看下面的 SQL 命令。

// Correct way
const insertQuery = `
          INSERT INTO public.data (geometry, name, type,id)
          VALUES (ST_setsrid(ST_GeomFromGeoJSON($1),4326), $2, $3,$4)
        `;

请注意字符串 $1$2$3$4。这些是我们插入的实际值的占位符。我们不应该使用 模板字面量 来格式化我们的字符串。在 JavaScript Node-postgres 中,你不应该这样做。

// DON'T DO THIS
let value: string;
const insertQuery = `
          INSERT INTO public.data (name)
          VALUES ("${value}")
        `;

你可能会让数据库面临恶意查询。例如,如果${value}的值是DROP TABLE命令,那么函数将执行DROP TABLE命令!如果我们使用$1参数化查询,这种情况是不会发生的。

使用空间 SQL 插入

现在让我们来看一下插入语句以及我如何处理空间数据。

-- inserting spatial data
INSERT INTO public.data (geometry, name, type,id)
VALUES (ST_setsrid(ST_GeomFromGeoJSON($1),4326), $2, $3,$4)

ST_GeomFromGeoJSON 是一个 postgis 函数,它接受一个字符串化的几何体并将其转换为几何对象。例如,

ST_GeomFromGeoJSON('{"type":"Point","coordinates":[-48.23456,20.12345]}')

然后我们将其包装在ST_SetSRID函数中以声明其投影。我在这里使用 EPSG 代码4326来表示这是经纬度值。你可能会收到不同投影的数据。例如,在英国,数据通常会投影到 British National Grid,其代码为27700;北坐标和东坐标。这对于这个演示不适用。

释放连接

最后,我们释放客户端,以便服务器可以处理其他客户端。

client.release(); 
pool.end();

运行代码

运行代码使用npm run start,这会运行npx tsc(这将 TypeScript 代码编译成 JavaScript)和node build(这实际上执行运行 ETL 过程的 JavaScript 代码)。日志显示操作成功。

执行代码(来源:作者,2023)

让我们通过选择数据在 PgAdmin 或普通的 psql 中查看数据。类似于,

SELECT name, type, id, geometry FROM data

使用 PgAdmin,我们可以看到数据在地图上是合理的!这意味着数据分析师现在可以开始分析数据了。

查看数据(来源:作者,2023)

我们还可以在 QGIS 中开始玩转数据,QGIS 是一个开源的 GIS 软件,我们可以用来分析地理空间数据。

使用 QGIS 查看数据(来源:作者,2023)

结论

空间数据工程的主要思想是创建一个数据管道,从一个来源提取原始数据,对其进行处理,并将其存储以供使用。这使得空间数据分析师可以专注于数据分析,而不必担心数据的可用性;这由空间数据工程师负责处理。空间数据工程的特殊之处在于我们如何处理空间数据,即使用空间 SQL。使用 TypeScript 和 Node.JS,借助 node-postgres,我们可以构建一个简单的数据管道。数据管道获取 geojson 数据,对其进行转换,并将其存储在启用了 Postgis 的 PostgreSQL 数据库服务器中。这些存储的数据随后可以使用 GIS 软件进行分析。

对我说话:一个模型阅读了多少个词

原文:towardsdatascience.com/speak-to-me-how-many-words-a-model-is-reading-331e3af86d27

| 人工智能 | LLM | 自然语言处理 |

| --- | --- | --- |

为什么以及如何克服大型语言模型的内在限制

Salvatore RaieliTowards Data Science Salvatore Raieli

·发表于 Towards Data Science ·阅读时间 20 分钟·2023 年 7 月 14 日

--

图片由 C D-X 提供,来源于 Unsplash

大语言模型(LLMs)在最近几个月展示了它们的技能,证明它们在各种任务中都很熟练。所有这些都通过一种交互模式:提示。

最近几个月,扩展语言模型的上下文成为了一种潮流。但这对语言模型有何影响?

本文分为不同的部分,每一部分我们将回答这些问题:

  • 什么是提示,如何构建一个好的提示?

  • 什么是上下文窗口?它能有多长?是什么限制了模型输入序列的长度?为什么这很重要?

  • 我们如何克服这些限制?

  • 模型是否使用了长上下文窗口?

如何与模型对话?

图片由 Jamie Templeton 提供,来源于 Unsplash

什么是提示?什么是好的提示?

简而言之,提示就是与大型语言模型(LLM)交互的方式。给定一个LLM,我们可以通过提供文本形式的指令来进行交互。这种文本提示包含了模型处理响应所需的信息。提示可以包含问题、任务描述、内容及其他许多信息。基本上,通过提示我们向模型提供了我们的意图以及我们期望它回应的内容。

提示可以显著改变模型的行为。例如,要求模型“描述法国的历史”与要求它“用三句话描述法国的历史”或“用说唱形式描述法国的历史”是不同的。

为了从模型中获取足够的信息,建议编写一个好的提示。通常,一个好的提示应该包含一个问题或一组指令。此外,还可以有上下文(问题 + 上下文)。例如,我们可以要求模型在一篇文章(上下文)中告诉我们主要人物是什么(问题)。

一般来说,编写提示时需要考虑一些元素:

  • 简洁性,由于这是一个迭代过程,最好从简单的问题开始,然后逐渐要求更多信息。此外,如果将模板减少为子任务的形式,模板效果最佳:简单任务而不是复杂任务。

  • 关于任务的指令。 使用能够明确指令的动词有助于模型更好地理解当前任务。此外,一些动词对于某些任务效果更佳,建议在提示的开头提供指令。

  • 具体性。 在提示中具体而注重细节有助于任务的执行。此外,可以提供示例来更好地解释所需内容(少量示例提示)。然而,由于提示的长度不是无限的,因此应避免提供过多的示例或过多的细节(在许多 LLM 中,提示在超过某一长度后会被截断)。

此外,还有其他技术可以用来改进提示,例如 chain-of-thought(强制模型回溯中间步骤)、自我反思(允许模型评估其响应)、思维树 等等。

[## 人工智能大学生回到实验台

LLM 如何解决大学考试以及为什么这很重要

levelup.gitconnected.com

一般来说,尽管其中一些技术很简单,但它们并不总是能产生良好的结果。还有更复杂的技术,提示工程仍然是一个开放的领域。这些技术的原则是让模型对问题进行推理或充分利用其在训练中学到的内容。

无论如何,所有这些技术都必须面临一个问题:提示中可以插入的 tokens (子词)的最大数量。

提示可以有多长,为什么?

提示的长度可以有多长:上下文的长度

patricia sernaUnsplash 的照片

提示可以迅速增长,尤其是当上下文包含大量信息(使用上下文中的文章、过去的对话、添加外部信息等)。这意味着模型必须处理长序列作为输入。

基本上,LLM 是一种 变换器,而变换器在序列长度上扩展不良。这是因为 变换器 建立在重复的 自注意力 模块上,这些模块在长度方面具有 二次成本

当然,以前已经有很多工作尝试通过各种策略减少这个成本。然而,线性替代的自注意力被发现表现不佳。

[## 欢迎回到 80 年代:变换器可能会被卷积所取代

Hyena 模型展示了卷积如何比自注意力更快

levelup.gitconnected.com](https://levelup.gitconnected.com/welcome-back-80s-transformers-could-be-blown-away-by-convolution-21ff15f6d1cc?source=post_page-----331e3af86d27--------------------------------)

自回归变换器在短序列上表现出色,但在处理长序列如高分辨率图像、播客、代码或书籍时表现不佳。 (source)

通常,上下文窗口 相对较小(512–1024 tokens)。然而,近年来我们已经看到有些模型甚至有成千上万个令牌用于 上下文窗口

例如,GPT-4 的上下文长度为 32k。除了这个数字令人印象深刻之外,这不仅仅是一个营销问题。实际上,上下文长度越长,模型能够关联的信息就越多。此外,更长的 上下文长度 可以提高准确性、流畅性,并被认为能激发模型的创造力。

从理论上讲,一个使用上下文长度为 1k tokens 训练的变换器确实可以在推断时生成 100k 的序列。但由于它是在不同的训练数据分布下训练的(远少于 100k 的序列),因此生成的结果将毫无意义。

事实上,已经证明,在ChatGPT中请求对超过 1000 个单词的文本进行拼写检查会导致模型幻觉

自注意力的二次成本意味着增加上下文长度等于训练成本的相应增加。LLaMA 的成本估计为 300 万美元(仅为 GPU 训练),将上下文长度增加 50 倍(从 2K 到 100K)也意味着成本增加 50 倍。

图片来源: 这里

自注意力是否是扩展上下文长度的唯一限制?

不是的。经过tokenization后,模型接受 tokens 序列,第一步是嵌入。对于一个长度为 n 的 tokens 序列,我们有一个大小为 d 的嵌入。显然,如果 n >> d,会有信息丢失的风险,而将 n 增加得远超过 d 则会带来显著的挑战。

此外,正弦位置编码器与某些扩展上下文长度的解决方案不兼容,需要重新考虑。

此外,训练是并行化的,但推断时计算是顺序进行的。事实上,一个 token 依赖于序列中生成的 tokens。因此,推断也必须优化以扩展上下文长度

如何实现数万甚至数十万 tokens 的上下文窗口?

扩展 LLM 的上下文窗口

照片由Nagara Oyodo拍摄,来源于Unsplash

让你的上下文更大

尽管过去几个月的结果令人印象深刻,但早在 2019 年就已经有尝试增加上下文窗口的长度。事实上,Transformer-XL能够生成连贯的文本,长度可达数千个标记。

作者利用了递归神经网络的理念,其中隐藏状态被重复使用,以便将更多信息提供给变换器。换句话说,在处理一个序列段后,在处理下一个段时,会重复使用之前获得的隐藏状态。从模型描述来看,与RNNs的相似性是显而易见的。

图片来源:这里

短期训练,长期推断

尽管 TransformerXL 是一个有趣的解决方案,但最近几个月也在测试其他策略。这些策略旨在解决原始变换器固有的局限性,并利用了今天的硬件进步。

减少训练成本的一个想法是用上下文长度为 2K 的模型进行训练,然后在更长的序列(例如,65K)上进行微调。理论上,这也可以奏效(模型在第一次训练中学习语言的一般表示,然后在后续任务中专门化处理更长的序列)。

实际上,使用原始变换器的这一策略注定会失败,2021 年的一篇论文中已对此进行了说明。正如作者所解释的那样,推断中更长的上下文长度的能力称为“外推”。

我们将外推定义为模型在验证过程中,输入标记数量超过模型训练时标记数量的情况下,继续表现良好的能力。我们发现使用正弦位置嵌入的变换器语言模型(LMs)具有非常弱的外推能力。(来源

图片来源:这里

对于作者来说,位置编码是缺乏原始变换器外推能力的罪魁祸首。位置编码,作为模型开始时的一步,被包含在内,作为一个巧妙的技巧,使模型能够考虑序列中每个标记的位置。

作者建议用带有线性偏置(ALiBI)的注意力替代它。简单来说,就是在注意力的查询和键的乘积中添加一个惩罚,这个惩罚与它们的距离成正比:

图片来源:这里

“在计算每个头的注意力得分时,我们的线性偏置注意力方法 ALiBi 会对每个注意力得分(qi · kj,左)添加一个常数偏置(右)。与未修改的注意力子层一样,然后将 softmax 函数应用于这些得分,其余计算保持不变。m 是一个特定于头的标量,在训练过程中设置且不进行学习。” 图片来源:这里

这个技巧很巧妙,因为它不会增加学习的参数,也不会显著增加计算成本

你需要所有这些标记吗?

扩展上下文窗口到超过 100k 个标记无疑非常有吸引力。另一方面,并非序列中的所有标记实际上都很有趣。那么,我们是否有必要计算这些标记之间的关系(注意力得分)呢?

因此,这个想法是利用稀疏性来计算注意力得分,这样我们就不会计算那些我们不感兴趣的标记之间的关系。正如谷歌所解释的,尽管如此,这并不是完全简单的:

两个自然的问题出现了:1) 我们是否可以使用计算和内存需求与输入序列长度线性相关的稀疏模型来实现二次全变换器的经验性收益?2) 是否可以理论上证明这些线性变换器保留了二次全变换器的表达能力和灵活性?(source)

谷歌尝试通过一个有趣的观察来回答这些问题,即注意力层可以被理解为一个。事实上,当计算序列中所有位置(节点)的注意力时,我们计算的是成对的相似度(边)。因此,从视觉上看,我们可以将注意力视为一个有向图

注意力作为一个完全图,图像由作者提供。

基于这一概念,可以看到,相对于经典的注意力,可以考虑使用图形,这些图形不是完全连接的。

谷歌通过首先利用全球和局部(或窗口)注意力的组合来利用这一概念,在这篇论文中进行,然后通过BigBird对这一想法进行了改进。

图片来源:这里

BigBird 基本上结合了三个概念:全球标记以覆盖整个序列,局部注意力以覆盖每个标记的周围区域,并且每个标记都有随机抽样的标记。

BigBird 成功地近似了注意力。同时,它是稀疏的(因此计算复杂度较低),但不会中断信息流(一个标记影响其他标记的能力)。

作者们证明了这种稀疏注意力不仅与原始注意力具有同样的表现力,而且可以用于从本质上较长的序列中提取上下文信息,例如基因组序列

在任何情况下,稀疏性概念都非常强大,许多研究人员正在尝试将其应用于其他模型,例如视觉变换器。

## META 的 Hiera:减少复杂性以提高准确性

简单性使得人工智能能够达到惊人的性能和惊人的速度

towardsdatascience.com

快速变换器的条件计算

基于并非所有令牌都重要的想法,还有另一种方法可以在训练过程中不将所有模型权重应用于所有令牌。

CoLT5利用这一概念来增加输入长度。简单来说,条件计算确保将更多资源分配给那些被认为重要的令牌。

作者构建了一个系统,其中注意力和前馈网络计算被分成两个分支(重型和轻型)。轻型层应用于所有令牌,而重型 MLP 仅应用于重要令牌。这些令牌由一个路由模块选择,决定哪些令牌是重要的。

图片来源: 这里

多查询注意力以节省计算

推理过程中,每个令牌的键和值都会被缓存(以避免在生成文本时重复相同的操作)。这在节省计算的同时增加了GPU的内存使用。

为了避免这种情况,多查询注意力(MQA)建议在键和值的线性投影步骤中对所有注意力头共享权重。

这在处理长序列时尤其有优势,减少了不仅仅是内存使用,还减少了生成时间。谷歌已展示了 MQA 的优势,在使用 PaLM 时

图片来源: 这里

闪光注意力,新一代 LLM 的亮点

之前看到的模型和思想旨在以减少其成本的方式修改注意力。闪光注意力使用了不同的方法,今天几乎所有模型都在使用它。

从基础上讲,GPU的利用更为高效。实际上,GPU有自己独特的内存层次结构。当GPU执行操作时,数据必须存在于快速内存(SRAM 内存)中。数据从 HBM 内存复制到这块内存中,一旦计算完成,输出结果会被复制回 HBM。

如你所见,SRAM 内存不仅速度更快,而且体积更小。随着时间的推移,计算变得越来越快,而 HBM 已经成为瓶颈。

图片来源: 这里

这是因为在注意力过程中,进行了一些操作(查询和键的乘法,softmax,将这个结果与值进行乘法)。这些操作生成的中间数据被复制到 HBM 和 SRAM(来回传输)。这种数据复制操作是导致操作变慢的原因。

SRAM 有内存限制,因此闪存注意力解决方案是将各种数组(查询、键、值)划分为块。因此,所有操作都在一个 GPU 内核中完成,然后将结果写入 HBM。此外,softmax 也随时间减少,因为它仅在块上计算,而不是整个 NxN 矩阵上。

图像来源:这里

不仅META 的 LLaMA使用 FlashAttention,今天virtually 所有模型都在使用它。

[## META 的 LLaMA:一个小型语言模型击败巨头

META 开源模型将帮助我们理解语言模型偏见的产生

medium.com](https://medium.com/mlearning-ai/metas-llama-a-small-language-model-beating-giants-5065948e0b7f?source=post_page-----331e3af86d27--------------------------------)

最新发展

最近在GPU方面的进展也使得令牌和它们的上下文增加成为可能。例如,现在有 80 GB 的 GPU。

此外,除了我们上面看到的技术改进,还有对变换器为何不具备外推能力的更好理解。例如,在这篇论文中,他们展示了经典注意力如何漂移到序列的后面位置(这一行为作者输入到正弦位置编码中)。因此,我们看到位置编码如何在 ALiBI 中被替代(其他人提出可以用随机版本、随机位置编码来替代)。

其他作者指出,如链式思维等技术有助于模型进行推断,因为模型必须专注于推理的中间步骤。此外,少量示例可以比微调更好地提高模型的推断能力(无论如何,这不是灵丹妙药)。实际上,使用一些技巧进行微调可以带来非常好的结果,例如 LLaMA 7B 在这项研究中引入了窗口注意机制,将上下文窗口从 2K 扩展到 32K。

然而,如果以前Claude的 100K 上下文长度看起来令人难以置信。META 的 Megabyte 声称支持 1M tokens(其技巧是,“Megabyte 将序列分割成补丁,并在补丁内使用局部子模型,在补丁之间使用全局模型”)。

图片来源:这里

一篇最近发表的论文甚至声称可以处理 1G 的 token。这些都表明,仍有大量的活跃研究,以及许多团队正在寻找扩展上下文长度的方法。

考虑到所有这些研究和替代方案,一个问题浮现:模型如何使用这些长上下文?它能否充分利用这些上下文?

在中间迷失:语言模型如何利用长上下文

图片由Ethan Sykes提供,来自Unsplash

最新的大型语言模型(LLM)的进展使得上下文窗口得以扩展,这引发了一个问题:模型是否真的从中受益?本月发表的一篇文章探讨了这一问题

这项研究的作者能够利用这样一个事实,即不仅像ClaudeGPT-4这样的专有模型具有长上下文窗口。实际上,MPT-30BLongChat-7B的上下文窗口分别为 8K 和 16K tokens。因此,作者决定使用这两种模型以及一些封闭模型(GPT-3.5Claude)。

选择模型后,还必须选择那些需要模型具备长上下文窗口的任务。例如,在多文档问答中,需要模型对一组不同的文档进行推理,以找到回答所需的信息。这是一个重要的任务,因为它模拟了在文档语料库(例如互联网)中进行搜索的事实(我们可以想象一个AI 搜索引擎需要搜索多个网站以找到答案)。

对于一个问题 x,有一组文档,其中只有一个文档包含回答问题所需的信息。如示例所示:

图片来源:这里

作者利用了注释问题的数据集(谷歌搜索)。他们随后添加了与主题相关但不包含答案的维基百科片段(小心确保正确的文档不总是位于相同的位置,因为LLM可能学会了启发式技巧)。

图片来源:这里

作者指出三个特别有趣的结果:

  • 他们注意到 U 形响应,取决于相关文档的位置。换句话说,当模型必须访问位于上下文窗口中心的信息时,模型表现会下降。因此,模型在识别相关信息时,如果信息位于输入序列的开头或结尾,表现会更好。

  • 性能低于闭卷设置。当相关文档位于输入上下文的中心时,模型的表现比没有提供文档时更差。在闭卷设置中,模型必须仅依赖于其参数的记忆。

  • 一般来说,如果向模型提供更多文档,性能会下降。

图片来源:这里

图片来源:这里

此外,作者指出,单纯来说,具有较长上下文窗口的模型并不优于其具有较短上下文序列的对应模型。

由于模型在使用位于上下文窗口中心的信息时表现不佳,作者想知道模型是否至少能够重新找到信息。换句话说,使用由键值对组成的简单文件(JSON),模型是否能够找到信息?

作者们决定使用尽可能简单的任务来深入研究模型的行为。这是一项基本技能,其中模型需要找到一条信息,而无需复杂的技能。使用模拟数据,作者们创建了包含键值对的JSON,其中只有一个是感兴趣的。

我们的合成键值检索任务旨在提供一个最小的测试平台,以测试从输入上下文中检索匹配标记的基本能力。[…]我们明确旨在通过尽可能移除自然语言语义(改用随机 UUID)来提炼和简化任务,因为语言特征可能会带来潜在的混淆因素 (source)

图片来源: here

结果显示,并非所有模型都能胜任,Claude在这项任务中表现成功,但其他模型在键值对达到 140 个或更多时性能会下降。

图片来源: here

此外,作者们观察到一个有趣的事实:

在 140 个键值对的设置中,LongChat-13B (16K)是一个显著的异常值;当相关信息位于输入上下文的开始部分时,它倾向于生成代码来检索键,而不是直接输出值。 (source)

为什么 LLM 不能充分利用较长的上下文窗口?

本研究的作者们想知道这是否与架构有关。目前,主要使用两种架构:仅解码器编码器-解码器语言模型。尽管它们在许多文章中被使用,但其行为差异仍存在模糊点。

因此,作者们决定另外使用两个模型:Flan-T5-XXLFlan-UL2。这两个模型在相关信息位于上下文窗口中间时表现出更强的鲁棒性。

图片来源: here

这很有趣,因为作为双向模型,编码器-解码器可能在处理较长的上下文窗口中的信息时更为鲁棒,因此在处理多个文档时可能更为高效。

长上下文窗口有用吗?

如果模型无论如何都无法充分利用它,那么拥有如此长的上下文窗口是否真的有意义?毕竟,拥有更长的上下文窗口无论如何都会有代价:模型必须处理所有输入信息。换句话说,如果模型能处理 100K 个 tokens,那么提供 100K 个信息点是有意义的。

作者决定使用检索系统进行测试,该系统接受一个输入查询(来自问题数据集的一个问题),并从维基百科中找到 k 个文档。然后,他们将这些文档添加到提示中,测试模型在这些附加文档下的表现。

使用超过 20 份检索文档仅能略微改善读者表现(GPT-3.5-Turbo 约 1.5%,Claude 约 1%),同时显著增加输入上下文长度(从而增加延迟和成本)。 (source)

图片来源:这里

换句话说,模型达到了饱和。如前所述,这证实了模型在上下文窗口的开头使用信息的效率更高。

结语

摄影师:Saif71.comUnsplash

提示是我们与模型互动的方式。它越精准和详细,模型的回应就越好。

然而,我们可以放入提示中的信息量是有限的。这个限制就是上下文窗口,它来源于我们之前看到的众多因素。自从第一个 transformer 发布以来,人们一直在尝试通过利用各种解决方案来扩大这个上下文窗口。

尽管如此,我们仍然不知道模型能够多好地利用这个上下文窗口。今天的研究显示,模型并未充分利用它们。

自从扩展法则发布以来,一直在追逐参数,不断增加模型的规模以寻找短暂的突现属性。如今我们知道,所有这些参数并非必要,GPT-4 实际上并不像想象中那么庞大,而是由八个模型组成。上下文窗口似乎是另一个前沿领域,人们试图达到更大的数量,不是为了实际效用,而是为了展示他们模型的优越性。

## LLMs 的无限巴别图书馆

开源、数据和注意力:LLMs 的未来将如何改变

towardsdatascience.com

尽管已经有大量的结果和发布的模型,但仍有一些问题需要研究。如何使用较长的上下文窗口就是其中一个需要进一步分析的点。因为尽管技术解决方案很优雅,但有时较长的上下文窗口的成本并不值得。

你怎么看?在评论中告诉我。

如果你觉得这很有趣:

你可以查看我的其他文章,也可以 订阅 以在我发布文章时获得通知,也可以 成为 Medium 会员 以访问所有故事(这是我通过平台获得的少量收入的附属链接,对你没有费用),还可以通过LinkedIn与我联系或找到我。

这里是我 GitHub 仓库的链接,我计划在这里收集与机器学习、人工智能等相关的代码和许多资源。

[## GitHub - SalvatoreRa/tutorial: Tutorials on machine learning, artificial intelligence, data science…

提供有关机器学习、人工智能、数据科学的教程,包括数学解释和可重用代码(使用 Python…)。

github.com

或者你可能对我最近的文章感兴趣:

[## CodeGen2: a new open-source model for coding

Salesforce 如何影响高效编码模型的设计

临床 GPT:LLM 临床医生 [## ClinicalGPT: the LLM clinician

今天的 LLM 已经被应用于各种任务。另一方面,通用模型在微调方面表现不佳…

临床 GPT:LLM 临床医生 ## 归纳偏差的神奇故事

我们需要归纳偏差吗?简单模型如何达到复杂模型的性能

towardsdatascience.com [## AI 大学生重返实验室

大型语言模型如何解决大学考试及其重要性

levelup.gitconnected.com](https://levelup.gitconnected.com/the-ai-college-student-goes-back-to-the-bench-daa6d9bdfb14?source=post_page-----331e3af86d27--------------------------------)

参考

这里是我撰写本文所参考的主要文献列表,只引用了文章的第一个名字。如果你想深入了解这个话题,我也建议你阅读这些文献。

  1. Prompt Engineering Guide, 链接

  2. Wang, 2023, 医疗保健中的提示工程:方法和应用, 链接

  3. White, 2023, 用于增强 ChatGPT 提示工程的提示模式目录, 链接

  4. Liu, 2021, 预训练、提示和预测:自然语言处理提示方法的系统调查, 链接

  5. Liu, 2022, 生成知识提示用于常识推理, 链接

  6. Sascha Heyer, 生成性 AI——LLM 提示工程的最佳实践, 链接

  7. Jacob Ferus, GPT-4 已经到来——这是你需要知道的, 链接

  8. Simon Attard, 为大型语言模型提供上下文, 博客文章, 链接

  9. Rickard, 大型语言模型商品化:第三部分

  10. Vaswani, 2017, 注意力机制是你所需的一切, 链接

  11. Press, 2021, 短训练,长测试:线性偏差的注意力实现输入长度外推, 链接

  12. Zaheer, 2021, Big Bird: Transformers for Longer Sequences, 链接

  13. Ainslie, 2020, ETC: 在 Transformer 中编码长而结构化的输入, 链接

  14. Google 博客, 使用稀疏注意力方法构建用于更长序列的 Transformer, 2021, 链接

  15. Shazeer, 2019, 快速 Transformer 解码:一个写头足矣,link

  16. Pope, 2022, 高效扩展 Transformer 推理,link

  17. Ahmed Taha,FlashAttention:快速且内存高效的精确注意力与 IO 关注,link,medium 文章

  18. Angelina Yang,什么是深度学习中的全局注意力?博客文章,link

  19. Dao, 2022, FlashAttention:快速且内存高效的精确注意力与 IO 关注,link

  20. 关于 FlashAttention 的深入分析 here

  21. Medium 上关于 LLaMA 的文章,META 的 LLaMA:一个击败巨头的小型语言模型

  22. Ainslie, 2023, CoLT5:具有条件计算的更快长范围 Transformers,link

  23. Yu, 2023, MEGABYTE:使用多尺度 Transformers 预测百万字节序列,link

  24. 一篇关于扩展上下文长度的最新进展的博客文章,link

  25. Andrew Lukyanenko,论文综述:将 Transformer 扩展到 1M tokens 及以上的 RMT,博客文章,link

  26. Liu, 2023, 迷失在中间:语言模型如何使用长上下文,link

说话探针:自解释模型?

原文:towardsdatascience.com/speaking-probes-self-interpreting-models-7a3dc6cb33d6?source=collection_archive---------20-----------------------#2023-01-16

语言模型能帮助解释自己吗?

Guy DarTowards Data Science Guy Dar

·

关注 发表在 Towards Data Science ·16 分钟阅读·2023 年 1 月 16 日

--

照片由 Kane Reinholdtsen 提供,来源于 Unsplash

在这篇文章中,我尝试了一个想法,即语言模型可以被引导解释来自其参数的向量。结果比你预期的要好,但仍然需要做很多工作。

与科学论文中的惯例一样,我使用“我们”而不是“我”(部分原因是因为这样可以让文本听起来不那么以自我为中心..)。

这实际上不是一项完整的工作,而更像是一个初步报告,关于我认为可能有用的一个想法,应该共享。因此,我只进行了基本实验以检验方法的合理性。我希望其他研究人员能从我开始的地方继续工作,看看我建议的方法的局限性是否可以克服。这项工作面向专业人士,但任何具有良好变换器知识的人都应该能轻松阅读。

介绍

近年来,许多可解释性方法在自然语言处理领域得到了发展 [Kadar et al., 2017; Na et al., 2019; Geva et al., 2020; Dar et al., 2022]。与此同时,强大的语言模型在该领域引起了轰动。有人可能会想知道强大的语言技能是否使语言模型能够对其内在状态进行交流。这项工作是对这一猜测的简要报告。在这项工作中,我们将设计自然语言提示,并将模型参数作为虚拟标记注入输入中。提示的目的是指示模型解释单词——但不是一个真实的单词,而是一个表示模型参数的虚拟标记。然后,模型生成一个继续提示的序列。我们将观察该技术解释模型参数的能力,我们已有现有解释。我们称这种新技术为“发言探针”。我们还将在高层次上讨论为什么可能期望这种方法有效的理由。

图示:发言探针的插图

鼓励可解释性研究人员使用发言探针作为指导分析的工具。我们并不 建议 不加区分地依赖其回答,因为它们并不完全可靠。然而,它们具有自然语言的表达能力这一重要优势。我们的查询在零样本情况下对模型来说是超出分布的,因为它仅用真实的标记进行训练。然而,我们假设它在操作其表示方面的固有技能将使其容易学习新任务。

我们为有兴趣自己探索这一技术的研究人员提供以下两个资源:

[## GitHub - guyd1995/speaking-probes

目前无法执行该操作。您在其他标签页或窗口中登录。您在其他标签页中退出了登录…

github.com

背景:残差流

这在我之前的帖子中的背景部分已更详细地解释: 在嵌入空间中分析 Transformers — 解释

我们依赖于通过其残差连接对变换器的有用视图,这一视图最初由 nostalgebraist [2020] 提出。具体而言,每一层将隐藏状态作为输入,并通过其残差连接向隐藏状态添加信息。在这种视图下,隐藏状态是沿层传递的残差流,从中读取信息,并在每一层写入信息。Elhage 等人 [2021] 和 Geva 等人 [2022b] 观察到残差流在最后几层通常几乎没有更新,因此最终预测是在早期层中确定的,隐藏状态大多在后续层中传递。残差流视图的一个令人兴奋的结果是,我们可以通过将隐藏状态与嵌入矩阵 E 相乘,将每层的隐藏状态投影到嵌入空间,将隐藏状态视为最后一层的输出。Geva 等人 [2022a] 使用这种方法来解释基于变换器的语言模型的预测,我们也采取了类似的方法。

图:残差流视图——可视化

展示说话探测

概述

我们的直觉基于残差流视图。在残差流视图中,模型的参数与 token 嵌入在隐藏状态上以或多或少平等的方式相加。更一般地,残差视图暗示考虑参数向量、隐藏状态和 token 嵌入使用相同“语言”的理由。从“句法上”来看,我们可以使用任何连续表示——无论是参数向量还是隐藏状态——作为虚拟 token。我们将“神经元”与“虚拟 token”交替使用。

本文将重点讨论参数,因为隐藏状态似乎更复杂——这很有道理,因为它们是参数的混合。我们展示了参数向量可以与 token 嵌入一起在输入提示中使用,并产生有意义的响应。我们假设神经元最终会崩溃成与其编码概念相关的 token。

我们的目标是利用语言模型具备的强大沟通技能来表达其潜在知识。我们建议几个提示,要求模型解释一个词。模型不会直接接收到一个词,而是一个代表参数向量的虚拟 token。我们用标签 在提示中表示虚拟 token(在运行模型时,其 token 嵌入会被我们想要解释的神经元替换)。然后,我们生成提示的继续部分,这就是语言模型的响应。

提示

The term "<neuron>" means
- Synonyms of small: small, little, tiny, not big
- Synonyms of clever: clever, smart, intelligent, wise
- Synonyms of USA: USA, US, United States
- Synonyms of Paris: Paris, city of lights, city of love, capital of
France
- Synonyms of error: error, mistake, fallacy, wrong
- Synonyms of <neuron>:
The term <neuron> is a broad term related to
From Wikipedia: "<neuron> is

更多示例可以在 prompts/ 仓库中找到

方法

我们将一个提示输入模型,并生成文本的延续,其中 的“token embedding” 是我们想要解释的神经元。为了产生多样化的输出,我们使用采样而不仅仅是贪婪解码。我们将看到下面的一些示例。

在这项工作中,我们将重点关注前馈(FF)键(前馈子层的第一层),因为它们似乎比 FF 值(第二层)更容易解释。每一层 l 都有一个矩阵 K_l(不要与 attention 键混淆)——它的每一列都可以被视为单独解释的。

为了测试我们的方法,我们使用了我们已经对其在嵌入空间中的意义有较好了解的模型。显然,这些是我们可以考虑的最简单的情况——所以这些实验只是为了基本检查。作为语法糖,我们使用 <param_i_j>** 来表示 i 层中第 j 个 FF 键的神经元。下面的所有示例都来自 GPT-2 medium。我们使用的生成超参数是:

temperature=0.5
repetition_penalty=2.
do_sample=True
max_new_tokens=50
min_length=1

示例:与嵌入空间投影的比较

日本名字

当投影到嵌入空间时,<param_10_8> 似乎与日本名字以及通常与日本相关的术语相关(就我所知):*

'Miy', '#imaru', 'Gong', 'Jinn', 'Xia', 'Makoto', 'Kuro',
'Shin', '#Tai', 'Yamato', 'Tai', 'Ichigo', '#Shin', '#atsu', 
'Haku', 'Chun','#ku', 'Qing', 'Tsuk', 'Hua', 'Jiang', 'Nanto',
'manga', 'Yosh', 'yen', 'Osaka', 'Qian', '#uku', '#iku', 'Yue',
'Choi', '#iru', 'Hai', '#uchi', '#Japan', 'Kyoto', '#kun',
'#ikan', '#ugu', 'Tsu'

让我们看看新方法给我们带来了什么:

The term "<param_10_8>" means

generate: The term " <param_10_8>" means the god of light.
It is said that a man named Kamiyama was born in this world, and thus
he became known as Kaname (風). The name for him comes from his
appearance while wearing an animal's skin

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <param_10_8>" means the spirit of a person, and
it is used to describe something like emotion. 
In other words there are two types: 「Hō」 (Emotion) or 「Ikuoi''(Spirit). 
The first type can be described as

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <param_10_8>" means a person of Japanese descent.
In the original, this is said to be an English word 
meaning 'a man with red hair'. However in later versions it's used as 
a derogatory reference towards people who speak poorly or do not 
understand their own language

讨论:虽然它没有给出明确的答案,但从以上示例中不难推测,该术语确实与日本和日本人相关。正如你所看到的,即使不涉及神经元,该模型也不太真实,像最后一个示例那样提出奇怪的断言。

进步

<param_11_2> 似乎与进步的概念相关。如果我们在嵌入空间中查看它,我们得到:

'progressing', '#Progress', '#progress', '#osponsors', '#oppable',
'advancement', 'progress', 'Progress', '#senal', '#venge', 'queue',
'#pun', 'progression', '#wagon', 'advancing', '#cknowled', '#Goal',
'momentum', '#zag', '#hop', 'pursuits', '#encing', '#Improve',
'STEP', '#chini', 'standings'

让我们看看新方法是否能得到这些:

The term "<param_11_2>" means

generate: The term " <param_11_2>" means to move forward.
If you're doing it right, your goal is not necessarily the same 
as yours or others' goals. You may be able for a  while (say 7-10 
years) at least achieve some of what they are achieving in

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <param_11_2>" means to move forward into 
the future. If you want a list of possible things that could 
happen in your life, this is what it looks like:
I have no idea how much I've learned or accomplished 
since then (but if anything has changed for

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <param_11_2>" means to make a substantial 
advance towards attaining the object of an undertaking.

    <!--end--> _______________________________________________ 
Sent through by email, sent from archive@gmane-online:...

讨论:我们可以认为这项结果相当不错。由于模型是基于互联网数据进行训练的,因此一些答案可能会复制网页讨论的格式。总的来说,这个参数似乎对说话探针易于解释。

以 G 开头的词汇

<param_0_0> 似乎与以字母“g”开头的词汇有关。在嵌入空间中,我们看到:

'#gd', '#GF', '#G', 'GN', '#GR', '#gat', 'Goff',
'#GW', 'g', '#GV', 'GS', 'Gw', '#GS', '#GBT',
'#gm', '#gp', 'Gork', 'Gow', '#GD', '#GP', '#gal',
'Gad', '#GC', '#g', 'GD', 'G', 'GA', '#gel', '#GG',
'Gemini', 'Gard', 'GR', '#Gs', '#gom', '#gran', 'gad',
'#gr', '#GM', '#GA', '#ga'

让我们看看使用说话探针会发生什么:

The term "<param_0_0>" means

generate: The term " <param_0_0>" means a character from the series.
(I'm not sure if this is an actual word, or just some sort of 
translation error.)
A lot has been written about how much we love Doctor Who: 
The Next Generation and what makes it such
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <param_0_0>" means a person who is not of 
Greek origin.
In the United States, there are many people with names that 
begin in G and end at g (such as: John Doe). The name was 
originally spelled 'John' but it became common to spell

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <param_0_0>" means a small (6.5mm) hole in 
the glass, which is formed when water flows through it and 
enters into an open space inside the lens of your camera. The 
image on this page was taken with my Olympus OM-D E M 50
- synonyms of small: small, little, tiny, not big
- synonyms of USA: USA, US, United States
- synonyms of Paris: Paris, city of lights, city of love, capital of France
- synonyms of error: error, mistake, fallacy, wrong
- synonyms of clever: clever, smart, intelligent, wise
- synonyms of <param_0_0>:

generate: - synonyms of small: small, little, tiny, not big
- synonyms of USA: USA, US, United States
- synonyms of Paris: Paris, city of lights, city of love, capital of France
- synonyms of error: error, mistake, fallacy, wrong
- synonyms of clever: clever, smart, intelligent, wise
- synonyms of <param_0_0>: G, man (male), woman(female)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: - synonyms of small: small, little, tiny, not big
- synonyms of USA: USA, US, United States
- synonyms of Paris: Paris, city of lights, city of love, capital of France
- synonyms of error: error, mistake, fallacy, wrong
- synonyms of clever: clever, smart, intelligent, wise
- synonyms of <param_0_0>: Gagarin rocket/rocket scientist 
(from the Russian "Gigafar")

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: - synonyms of small: small, little, tiny, not big
- synonyms of USA: USA, US, United States
- synonyms of Paris: Paris, city of lights, city of love, capital of France
- synonyms of error: error, mistake, fallacy, wrong
- synonyms of clever: clever, smart, intelligent, wise
- synonyms of <param_0_0>: gg, go

讨论:使用第一个提示时,探针似乎与参数的嵌入空间解释无关。我们在正确方向上的唯一线索是来自第二个示例的“在美国,有许多人名字以 G 开头,以 g 结尾...”。然而,这种说法是间接的,并不是对问题的直接回答。

这是展示说话探针灵活性的好地方,因为它们允许你重新表述问题,并可能获得更好的结果。可以说第二个提示更适合问题。虽然它不完全真实,但对响应的概述可以暗示正确的方向——一个样本回应“G”然后给出几个无关的词。另一个则回复“gg, go”。虽然不明确,但它可以帮助开始探索。通常建议使用更多样本。

合成示例

也可以创建我们知道它们“应该”意味着什么的神经元。例如,我们取两个令牌嵌入的平均值,看看模型能否通过适当的提示重建它们。这有助于我们调试方法。我们可以根据不同变体的反应来评估其反应,并基于这些改进我们关心的实际神经元的提示。

在下面的示例中,我们将“children”和“dog”的令牌嵌入加在一起。然后我们对其应用一个说话探针。

我们可以用这个神经元作为探针处理多义神经元的玩具示例。不过要记住,行为可能会根据词汇的不同而有所不同。

generate: The term " <neuron>" means a person under the age of 18 years.
 The terms are used in this chapter as follows: 
(1) A child shall be considered to have attained legal maturity when he 
or she has reached an understanding with his parent, guardian and other 
persons that

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <neuron>" means any animal that is capable of 
walking on two legs, but not a dog.
The phrase 'the children' refers to those who were born before 
the adoption law was changed in 1996 and are now considered as age 18
or 19 years old

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The term " <neuron>" means any member of the genus 
Canis familiarus, but does not include chimpanzees or gorillas.
(2) (a)(i), in paragraph 1, is used to mean a dog that has been 
domesticated by human beings and whose natural

探针对神经元的模糊性处理得相当好。样本似乎指向一个令牌,但然后可能会混淆并讨论另一个。

让我们看看当我们结合一些更具挑战性的令牌时会发生什么:“google”和“show”。这次我们使用一个不同的提示:

The invented word ' <neuron>' is a combination of the words

generate: The invented word'<neuron>' is a combination of the words 
google and gmail. This term was coined in 2006 by Google's Senior 
Vice President, Eric Schmidt to describe how people use their email 
accounts (Google+).
I have spent many hours trying on different web sites with Gmail for 
various reasons: I

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The invented word'<neuron>' is a combination of the words 
google and gopher, which mean both internet search engines. There are 
two main types: Google-based sites that can be visited on your computer 
(e.g., http://www!google) or from other web browsers like Firefox & 
Opera

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The invented word'<neuron>' is a combination of the words 
google and gmail. The first example I found was from 2002, but it has 
been used since then by many people on various websites to describe 
their web pages (see for instance this article).
If you like what we do, please consider supporting

看起来我们在这里运气用尽了。词语“show”似乎已经被“google”吸收了。我们来试着通过将“google”的令牌嵌入系数设置为更小的值来缓解这个问题——我们将其乘以 0.9,得到:

The invented word ' <neuron>' is a combination of the words

generate: The invented word'<neuron>' is a combination of the words 
show and giggle. A gag was coined in 1971 by British writer John Updike, 
who wrote it to describe an episode where TV presenter Peter Jennings 
tries his hardest not laugh at something he's seen before (see below).

Gag

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The invented word'<neuron>' is a combination of the words 
show, tell and lie ('tell'). This can be used to describe several types:
It's not just for kids. It also applies here at Wikipedia!

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

generate: The invented word'<neuron>' is a combination of the words 
show and giggle. In all probability, this was used to describe something 
that had no physical connection with comedy at first sight:
Gretchen Rubin's new book The Giggling Brain has just been published 
by Oxford University Press; it looks

似乎它可以将“show”“giggle”恢复为“google”的扭曲版本。

讨论

方法的潜力

我们希望利用此方法的独特特征是:

  • 自然语言输出:既是优点也是缺点,它使得输出更难评估,但比其他方法提供了更大的灵活性。

  • 操作潜在表征的固有能力:我们使用模型自身操作其潜在表征的能力。我们假设它们与模型参数共享相同的潜在空间,因为残差流视图。其他技术需要经过训练或以其他方式调整到模型的潜在空间以“理解”它。模型能够自然解码自身状态,这对解释是有用的。

一般来说,关于将连续向量作为变换器中的一等公民的研究不多。虽然像提示调优 [Lester et al., 2021] 这样的想法,以及像 Hao et al. [2022] 这样的令人兴奋的想法将连续输入传递给模型,但它们需要训练才能工作,且不能零样本使用。这项工作中的核心主题是研究是否可以在不进一步训练的情况下将某些连续向量用作自然标记——假设它们使用与模型相同的“语言”。

这种技术的另一个有用特性是,它或多或少将模型视为黑箱,而不涉及太多技术工作。它易于实现和理解。将解释视为生成问题,我们可以利用主流 NLP 的生成文献来进行未来的工作。类似地,幻觉是发言探针中的一个主要问题,但我们可以希望未来将主流研究方法应用于这种方法。

总体而言,这可能是 最模块化 的可解释性方法——它不依赖于特定的算法,并且可以采用 NLP 其他领域的见解进行改进,而不会丧失效率。此外,它易于实验(即使是对学术不太感兴趣的从业者),并且搜索空间的格局与其他方法大相径庭。

可能的未来方向

  • 雄辩。过于雄辩:语言模型被训练以产生雄辩的解释。事实性对它们来说较少令人担忧。这些雄辩的解释不应被字面理解。

  • 层同质性:在本文中,我们隐含地假设我们可以从不同的层获取参数,它们会对我们的提示做出类似的反应。可能有些层比其他层更适合与发言探针一起使用。我们称之为 层同质性。我们需要谨慎地假设所有层在我们的方法中可以被相同对待。

  • 神经元多义性:尤其是在面对词汇崩溃时,似乎携带多个无关解释的神经元需要多次采样,以涵盖所有不同的含义。我们希望能够更真实地提取这些不同的含义,并“一次性”完成。

  • 更好的提示:这不是我们工作的主要部分,但许多论文展示了使用精心设计的提示的好处 [例如,Liu et al., 2021]。

  • 其他类型的概念:我们主要讨论了表示自然语言中的类别或概念的神经元。我们知道语言模型可以处理代码,但我们在这篇文章中没有考虑这类知识。此外,使用发言探针来定位模型参数中的事实也很有趣。事实可能需要多个参数协同工作——因此,定位这些事实并找到能够提取这些事实的提示将会很有趣。

如果你进行后续工作,请引用如下:

@misc{speaking_probes,
      url = {https://towardsdatascience.com/speaking-probes-self-interpreting-models-7a3dc6cb33d6},
      note = {\url{https://towardsdatascience.com/speaking-probes-self-interpreting-models-7a3dc6cb33d6}},
      title = {Speaking Probes: Self-interpreting Models?},
      publisher = {Towards Data Science},
      author = {Guy, Dar},
      year = 2023
}

你也可以在 Twitter 上关注我:

[## Guy Dar (guy__dar) / Twitter

Twitter](https://mobile.twitter.com/guy__dar?source=post_page-----7a3dc6cb33d6--------------------------------)

这不是直接的后续,但你可能还对我与合作者合作的相关论文的另一篇博客帖子感兴趣:

## Analyzing Transformers in Embedding Space — Explained

在这篇文章中,我介绍了 Guy Dar、Mor Geva、Ankit Gupta 等人于 2022 年发表的论文《分析嵌入空间中的转换器》。

towardsdatascience.com

References

Dai D, Dong L, Hao Y, Sui Z, Chang B, 以及 Wei F. 预训练转换器中的知识神经元,2021. URL arxiv.org/abs/2104.08696

Dar G, Geva M, Gupta A, 以及 Berant J.《在嵌入空间中分析转换器》,2022. URL arxiv.org/abs/2209.02535

Elhage N, Nanda N, Olsson C, Henighan T, Joseph N, Mann B, Askell A, Bai Y, Chen A, Conerly T, DasSarma N, Drain D, Ganguli D, Hatfield-Dodds Z, Hernandez D, Jones A, Kernion J, Lovitt L, Ndousse K, Amodei D, Brown T, Clark J, Kaplan J, McCandlish S, 以及 Olah C. 转换器电路的数学框架,2021. URL transformer-circuits.pub/2021/framework/index.html

Geva M, Schuster R, Berant J, 以及 Levy O. 转换器前馈层是键-值存储器,2020. URL arxiv.org/abs/2012.14913

Geva M, Caciularu A, Dar G, Roit P, Sadde S, Shlain M, Tamir B, 以及 Goldberg Y.《Lm-debugger: 一个交互式工具,用于检查和干预基于转换器的语言模型》[Lm-debugger: An interactive tool for inspection and intervention in transformer-based language models]. arXiv preprint arXiv:2204.12130, 2022a。

Geva M, Caciularu A, Wang K R, 以及 Goldberg Y.《转换器前馈层通过推动词汇空间中的概念来构建预测*,2022b。URL arxiv.org/abs/2203.14680.

Hao Y, Song H, Dong L, Huang S, Chi Z, Wang W, Ma S, 以及 Wei F. 语言模型是通用接口,2022. URL arxiv.org/abs/2206.06336

Kadar A, Chrupala G, 以及 Alishahi A. 在递归神经网络中表示语言形式和函数。计算语言学, 43(4):761–780, 2017。ISSN 0891–2017. doi: 10.1162/COLI a 00300. URL doi.org/10.1162/COLI_a_00300

B. Lester, R. Al-Rfou 和 N. Constant. 参数高效提示调整的规模力量,2021. 网址 arxiv.org/abs/2104.08691

J. Liu, D. Shen, Y. Zhang, B. Dolan, L. Carin 和 W. Chen. 什么构成了 GPT-3 的优秀上下文示例?CoRR, abs/2101.06804, 2021. 网址 arxiv.org/abs/2101.06804

S. Na, Y. J. Choe, D.-H. Lee 和 G. Kim. 发现 CNN 单元中的自然语言概念,2019. 网址 arxiv.org/abs/1902.07249

nostalgebraist. 解释 GPT:logit 视角,2020. 网址 www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens

我们正在变化的全球村

原文:towardsdatascience.com/special-feature-our-expanding-global-village-2d8355436d2c?source=collection_archive---------7-----------------------#2023-03-15

TDS 特色专栏

扩张的城市与缩小的乡村

TDS 编辑面向数据科学 TDS 编辑

·

关注发表于Towards Data Science · 阅读时间:8 分钟 · 日期:2023 年 3 月 15 日

--

图片来自 Unsplash,创作者:Louis Reed

你是一个城市居民吗?你并不孤单!我们的全球城市人口已达56%,并预计到 2050 年将飙升至所有人口的70%。¹ 到那时,世界上的人类总人口将达到97 亿,² 这一我们居住地的变化将对我们生活的方式产生深远的影响。我们已经看到智能城市、机器学习模型以及地理空间监测正在为可持续性和充满活力、宜居的社区带来令人兴奋的新想法。

如果你从事数据科学、机器学习或 AI 工作,并希望了解你的技能如何帮助塑造讨论,或者如果你想寻找加入这一努力的灵感但不确定从何开始,你来对地方了。

借助 TDS 深厚的档案,我们收集了30 篇文章,以提供对数据科学和人口相关问题的全面视角。我们分享了地理空间分析师、建筑师和在城市规划、供应链优化以及野生动物保护等专业领域工作的作者的经验和实用解决方案。

我们大致将这些文章组织成呈现数据科学家正在解决的全球和本地挑战的主题。浏览标题并挑选你感兴趣的内容,或深入探讨单一主题。不管你如何使用这个资源,你一定能学到一些新东西。

当你准备好迈出第一步,撰写关于你自己的人口问题相关分析或解决方案时,你会在本指南的最后找到9 个有用的演练和数据集

我们鼓励你在阅读的文章上留下评论,并尽可能广泛地分享这些文章和本课程。

让我们开始吧!

1. 人口分析

我们从使用人口分析和可视化的全球鸟瞰图开始。这是一个重要的第一步,因为了解我们的人口如何分布,特别是与资源的比较,是找到量身定制解决方案的关键,这些解决方案能够带来最大的影响。

这些文章中有许多集中于地理空间分析,而其他则提供了图形或 3D 方法。每篇文章都提供了详尽的演练和良好的数据集来帮助你入门。

2. 城市规划

接下来,我们将重点关注在人口分析成为规划中心的城市,从住房和交通到商业服务和公用事业。我们将分享涉及城市规划问题、智慧城市和创新建筑方法的文章。

  • 计算创意凯伦·阿斯马尔 撰写(14 分钟)

    一种对生成对抗网络(GAN)在建筑中的有趣应用,可能揭示出未来城市的“不可思议建筑”。

  • 如何创建和使用轮廓线海伦·麦肯齐 撰写(8 分钟)

    轮廓线的使用可以确定服务可达性,这是成功城市发展的一个重要因素。

  • 预测长期每日市政水需求布莱克·范伯洛 撰写(13 分钟)

    深入了解加拿大伦敦市如何使用机器学习预测模型来预测水需求。

  • 利用表观时间非侵入式负载监测进行能源管理林多·圣安吉尔 撰写(15 分钟)

    该深度学习模型的重点是实现“可持续和可扩展的电网”在消费者层面的非侵入式负载监测的可行性。

  • 能源优化水平的比较分析克里斯蒂安·埃尔扬德 撰写(7 分钟)

    智能能源控制优化有可能显著降低家庭能源费用,包括与电动车充电相关的费用。

  • 人工智能如何助力智慧城市倡议Tirthajyoti Sarkar 撰写(8 分钟)

    深入探讨人工智能在处理与智慧城市倡议相关的大量数据中的角色,这些数据有助于规划者设计从交通管理到水资源的所有内容。

  • 使用开源数据估算太阳能面板输出Ang Li-Lian 撰写(9 分钟)

    LiDAR 可以作为估算太阳能面板输出的有效方法,如本全面指南的作者所述。

3. 可持续农业

每个国家在确保可靠食品供应方面面临着独特的挑战,许多国家依赖于从其他国家进口的农产品。我们确实是一个全球村!由于气候变化,农业实践已经开始发生变化,随着我们追求超高效和可持续的食品生产,还会看到更多变化。不要只听我们的说法——这些作者提供了关于现代农业未来的深刻文章。

  • 农业经济学优化与可持续发展Bonny Nichol 撰写(7 分钟)

    预测土壤、降雨、播种和施肥如何影响产量,提供了一种数据驱动的最佳实践,这可能帮助农民提高作物产量。

  • 使用 Google Earth Engine 和 FAO WaPOR 数据监测非洲水资源短缺(SDG 6.4)Iman Tantawy 撰写(5 分钟)

    地理空间监测以衡量有效的水资源使用,以满足农业需求。

  • 一种更易获取和可复制的加州卫星基础的手工采摘作物映射方法Madeline Lisaius 撰写(12 分钟)

    劳动力短缺对手工采摘的水果和蔬菜作物的影响,通过卫星图像找出种植模式的变化。

  • 通过数据策略应对全球食品挑战——约翰·迪尔的故事Ekhtiar Syed 撰写(6 分钟)

    智能农业结合 AI、AR 和远程监控的未来标准。

  • 使用深度学习帮助非洲农民提高产量Patrick Kalkman 撰写(12 分钟)

    一种深度集成学习模型,用于从图像中检测植物疾病,从而改善作物健康。

  • 神经网络会梦想飘落的雪吗?Fraser King 撰写(13 分钟)

    关于使用机器学习预测降雪的新研究,这是农业区域淡水的重要来源。

我们不断扩展的自然环境

随着我们占用更多土地以容纳和养活不断增长的人口,我们的自然环境受到影响。我们是一个复杂而脆弱的生态系统的一部分,需要精心维护,以造福所有人。这些作者提出了创新的野生动植物管理和监测策略,并详细探讨了土地使用变化对大气碳水平的影响。

  • 利用卫星影像弥合碳排放清单中的土地使用差距Guilherme M. Iablonovski 撰写(9 分钟)

    GIS 技术用于捕捉土地使用变化对城市树木、森林和土壤从大气中移除碳的影响。

  • 使用 Google Earth Engine 监测植被Sixing Huang 撰写(10 分钟)

    卫星影像如何用于评估和监测生态系统健康。

  • AI 地理空间野火风险预测Theo Jaquenoud 撰写(16 分钟)

    一个利用栅格数据进行野火风险预测的模型,旨在最小化火灾的危害及其相关成本。

  • 人口动态的形态Francesco Palma 撰写(7 分钟)

    通过拓扑数据分析识别和管理面临风险的野生动物种群。

  • 利用地理空间技术揭示对海洋巨型动物的捕鱼活动风险Bryan R. Vallejo 撰写(7 分钟)

    空间数据分析作为保护和保存海洋资源的有效工具,因为扩张的捕鱼活动与海洋生命竞争。

  • 构建一个简单的 AI 驱动的、在环系统以管理野生动物相机捕获的图像和注释Abhay Kashyap 撰写(19 分钟)

    计算机视觉作为一种成本效益高的解决方案,替代昂贵且劳动密集的人工标注大量野生动物相机捕获的照片,这是野生动物自然栖息地变化的重要第一步。

交通

最后,我们探讨人们和商品在本地和全球范围内的流动。随着我们人口的增长,车辆、船只和飞机的环境成本必须考虑,但随着道路交通量的增加,安全也成为问题。这些文章的作者分析了当前情况,并提出了解决方案,如共享单车、公共交通、智能道路和供应链优化。

  • 通过可视化公共航空数据审查航空公司效率Aine Fairbrother-Browne 撰写(10 分钟)

    对比 COVID 前后航空公司航班数据,以识别“幽灵”航班(低于 10%容量),这类航班具有较高的环境成本。

  • 使用 Python 进行供应链优化Samir Saci 撰写(10 分钟)

    最小化公司碳足迹,同时满足需求和降低运输成本,是这一优化策略的重点。

  • 空间数据科学:交通规划的网络分析Sutan Mufti 撰写(6 分钟)

    使用图论方法优化运输路线并模拟新/变化路线对可达性的影响。

  • 通过大数据分析支持非洲的辅助交通流动性 作者Marco Brambilla(6 分钟)

    大数据可能在解决交通贫困问题中发挥作用,通过提供可靠、负担得起的交通服务。

  • 智能道路:人工智能如何在交通中保护司机的安全 作者Andrey Koptelov(6 分钟)

    人工智能能否在车辆数量随着人口增长而增加的情况下提供安全的智能道路?

  • 利用广义加性模型(GAMs)生成洞察 作者Alvaro Peña(10 分钟)

    介绍如何使用广义加性模型促进“零排放交通”,使自行车共享成为一种简单且便利的选择。

想开始撰写自己的文章吗?

我们希望这篇特别特辑中的文章能激发你尝试自己的分析。如果你不确定从哪里或如何开始,我们可以帮助你!

首先,我们分享了两个精彩的与人口相关的数据集供你探索:

接下来,我们收集了一些来自我们作者的文章,他们分享了找到的数据集或提供了逐步指南,帮助你入门特定主题。这些都是启动你的文章的绝佳选择——也许会在我们的下期通讯中被提到!

我们希望你已经在我们的阅读列表中找到了一些感兴趣的文章,并且受到启发去研究一个或两个话题。当你准备好与我们的社区分享你对与人口相关挑战的数据科学分析时,我们鼓励你提交你的文章。

我们非常期待在我们不断变化的全球村庄中看到你对未来的展望!

参考资料

  1. 世界银行,城市发展,最后更新于 2022 年 10 月 6 日。

  2. 联合国,全球问题:人口,访问日期:2023 年 3 月 13 日。

专门化的 LLM:ChatGPT、LaMDA、Galactica、Codex、Sparrow 等

原文:towardsdatascience.com/specialized-llms-chatgpt-lamda-galactica-codex-sparrow-and-more-ccccdd9f666f

创建更好领域特定 LLM 的简单技巧

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

·发表于Towards Data Science ·阅读时间 30 分钟·2023 年 1 月 13 日

--

(照片由NASA提供,来源于Unsplash

大型语言模型(LLM)是非常有用的任务无关的基础模型。但是,我们实际上能用一个通用模型完成多少任务? 这些模型擅长解决我们在深度学习文献中看到的常见自然语言基准。然而,实际使用 LLM 通常需要教给模型与特定应用相关的新行为。在这个概述中,我们将探讨针对各种使用案例专门化和改进 LLM 的方法。

我们可以通过使用领域特定的预训练、模型对齐和监督微调等技术来修改 LLM 的行为。这些方法可以用来消除 LLM 已知的限制(例如,生成不正确/有偏见的信息),调整 LLM 的行为以更好地满足我们的需求,甚至将专业知识注入 LLM,使其成为领域专家。

最近的文献中对为特定应用创建专门的 LLM(大型语言模型)的概念进行了深入探讨。尽管存在许多不同的方法,但它们都有一个共同的主题:使 LLM 在实际应用中更具可行性和实用性。尽管“实用”的定义在不同应用和用户中差异很大,但我们将看到,存在几种技术可以用来调整和修改现有的预训练 LLM,从而在各种应用中显著提升其性能和易用性。

(来自[6]和[12])

背景

我们在最近的帖子中讨论了语言模型(LMs)和大语言模型(LLMs)的主题。有关这些概述,请参见以下参考文献:

  • 语言模型:GPT 和 GPT-2 [博客]

  • 语言模型的扩展定律与 GPT-3 [博客]

  • 现代大语言模型:MT-NLG、Chinchilla、Gopher 及更多 [博客]

我们将在这个概述中简要总结这些观点。但我们主要将关注于基本语言建模单独难以胜任的应用领域。

仅仅教一个模型预测序列中的下一个词,我们能实现的有限。为了引发特定行为,我们需要采用一些新的训练语言模型的方法,这些方法更具针对性。除了在提高语言模型质量方面非常有效外,我们将看到,这些替代的方法相比从头开始进行预训练要便宜很多。

什么是语言模型?

自监督的语言模型预训练(由作者创建)

基本设置。 我们将讨论的大多数现代语言模型采用仅解码器的 Transformer 架构[1]。这些模型被训练来执行一个简单的任务:预测序列中的下一个词(或标记)。为了让模型做到这一点,我们从互联网上收集大量未标记的文本数据,并使用自监督的语言建模目标来训练模型。简单来说,这意味着我们:

  1. 从我们的数据集中抽取一些文本

  2. 尝试用我们的模型预测下一个词

  3. 根据正确的下一个词更新我们的模型

如果我们不断重复这个过程,使用足够大和多样化的数据集,我们将得到一个高质量的语言模型,具有相对细致和有用的语言理解。

这有什么用? 虽然语言模型在生成文本方面显然很出色,但我们可能会怀疑它们是否对其他方面也有用。我们究竟能通过仅仅预测序列中最可能的下一个词来完成什么?

实际上,我们可以用语言模型解决许多不同的任务。这是因为它们的输入-输出结构(即,以文本为输入,产生文本为输出)非常通用,许多任务可以通过提示技术重新制定以适应这种结构。例如,考虑以下输入。

  • “识别这个句子的情感是积极的还是消极的:

  • “将以下句子从英文翻译成法文:

  • “总结以下文章:

使用这样的输入提示,我们可以将常见的语言理解任务转化为 LM 友好的、文本到文本的结构——LM 最可能的输出应该解决我们期望的问题。通过这种方法,我们可以解决从选择题回答到文档总结等广泛的问题,正如 GPT-3 所示 [2]。

(来自 [2])

为了提高性能,我们可以在提示中包含正确输出的示例(即,一次/少量示例学习方法),或者对 LM 进行微调以解决特定任务。然而,微调会强迫 LM 专注于解决单一任务,这需要为每个新任务微调一个单独的模型;见上文。

规模化。 早期的 LM 如 GPT 和 GPT-2 展现了很大的潜力 [3,4],但它们的零样本/少样本表现较差。然而,后来的研究表明 LM 的表现应该随着规模的增加而平稳提升 [5]——更大的 LLM 更好!这在 GPT-3 [2] 中得到了确认,这是一个具有 1750 亿参数的模型(即,比任何以前的模型都大得多),在少量示例学习方面表现非常好。这一成功的秘密在于:

  1. 获得一个大而多样化的未标记文本数据集

  2. 使用语言建模目标在这个数据集上预训练一个更大的模型

  3. 使用提示通过少量示例学习解决任务

使用这些简单的要素,我们可以训练出在许多任务中表现出色的大型语言模型(LLMs)。这些 LLM 是强大的、与任务无关的 基础模型

鉴于更大的 LLM 表现良好,后续的研究探索了更大的模型。结果(可以说)并没有突破性的进展。但是,如果我们将更大的模型与更好的预训练数据集结合起来,LLM 的质量会显著提高!通过获得更好的预训练语料库(例如,Massive Text)并在更多数据上预训练 LLM,我们可以获得如 Chinchilla 这样既更小又更高效的模型,相对于 GPT-3 更具性能。

通用 LLM 有哪些不足之处?

这种预训练 LLM 并用它们解决各种下游问题的通用范式非常好。但在尝试完成比一般语言理解更具体的任务时,我们会遇到问题。为了这篇文章的目的,我们将重点关注两个主要领域,这些领域中对更专业 LLM 行为的需求会出现:

  • 对齐

  • 领域专业化

将语言模型对齐到人类价值观(由作者创建)

对齐。 许多时候,通用 LLM 会生成对与模型互动的人不期望的输出。例如,我们可能想要:

  • 防止我们的 LLM 存在种族歧视

  • 教会模型遵循和执行人类指令

  • 避免生成事实错误的输出

换句话说,我们可能希望将 LLM 对齐到使用模型的人类的特定目标或价值观;见上文。

在像 GPT-3 这样的强大 LLM 基础模型创建之后,LLM 研究的重点迅速转向对齐问题。虽然描述起来有些模糊(即,我们如何定义我们对齐 LLM 行为的规则?),但对齐的想法非常强大。我们可以简单地教我们的 LLM 以对我们人类更安全和有用的方式进行行为。

许多最近大型语言模型使用的语言建模目标——从互联网网页上预测下一个词——与“帮助和安全地遵循用户指令”的目标不同——来自 [6]

领域特定模型。 除了对齐之外,我们可以考虑在专业领域中部署 LLMs。像 GPT-3 这样的通用 LLM 无法成功生成法律文件或总结医学信息——像法律或医学这样的专业领域包含大量复杂的领域知识,这些知识在通用预训练语料库中并不存在。对于这种应用,我们需要以某种方式创建一个对我们感兴趣的特定领域有更深知识的 LLM。

精细化 LLM 行为

(来自 [13])

鉴于我们可能希望将我们的 LLM 对齐到特定目标或实现更专业的行为,可能会立即想到两个主要问题:

  1. 我们如何做到这一点?

  2. 这将花费多少?

第一个问题有点复杂,因为有几种可行的答案。

领域特定预训练。 如果我们希望我们的 LLM 对某一特定领域有很好的理解,最简单的做法是(i) 收集大量与该领域相关的原始数据,并(ii) 使用语言建模目标对这些数据进行训练。这样的过程与通用 LLM 预训练非常相似,但我们现在使用的是领域特定的语料库。

通过学习更具体的语料库,我们可以开始在模型中捕捉到更多相关的信息,从而实现更专业的行为。这可能包括“提示预训练”等内容,如上图所示,我们会在与实际使用场景匹配的特定提示示例上进一步预训练 LLMs。

在进行领域特定预训练时,我们有两个基本选项:

  1. 用通用预训练初始化 LLM,然后在领域特定数据上进行进一步预训练。

  2. 从领域特定数据开始从零预训练 LLM。

根据应用情况,这些方法中的任何一种可能效果最好,尽管用预训练 LLM 参数初始化通常会导致更快的收敛(有时表现更好)。

从人类反馈中学习的强化学习。 仅仅使用语言建模目标,我们无法明确地做诸如教 LLM 遵循指令或避免错误陈述等事情。为了实现这些更微妙(且可能模糊)的目标,最近的研究采用了强化学习(RL)方法。

对于那些不熟悉 RL 的人,可以查看链接 这里 以了解该概念的基本概述。对于 LLM 应用,模型的参数对应于我们的策略。人类将向 LLM 提供输入提示,LLM 将生成响应输出,奖励由 LLM 的输出是否符合人类期望来决定。

尽管 RL 并不是必须的(即,若干研究专注于不使用 RL 的 LLM 对齐),但它非常有用,因为我们可以将“期望”的定义更改为几乎任何东西。例如,我们可以奖励 LLM 生成事实正确的陈述、避免种族主义行为、遵循指令或产生有趣的输出。这些目标通过可以用梯度下降优化的可区分损失函数很难捕捉。然而,使用 RL 时,我们只是奖励模型我们喜欢的行为,这提供了极大的灵活性。

(来源 [6])

大多数研究使用一种称为从人类反馈中学习的强化学习(RLHF)的方法来调整 LLM;见上文。RLHF 的基本思想是利用人类提供反馈,通过 RL 使模型学习。更具体地说,模型使用 近端策略优化(PPO) 进行训练,这是一种最近的、有效的 RL 方法。

监督微调。 我们还可以直接微调 LLM 来完成特定任务。这在如 GPT [3] 的语言模型中很常见,这些模型采用预训练和微调的方法,我们对预训练的语言模型进行微调以解决每个下游任务。最近,我们看到监督微调被用来修改 LLM 行为,而不是专门用于特定任务。

例如,如果我们想创建一个非常好的 LLM 聊天机器人呢?一个潜在的方法是获取一个通用的、预训练的 LLM,然后向这个模型展示一堆高质量的对话示例。然后,可以在这些对话示例上训练 LLM,从而使模型学习到更专业的行为,这些行为特定于这个应用,并使其成为更好的聊天机器人!

对齐成本很低! 大多数修改 LLM 行为的方法在计算上并不昂贵,尤其是相比从头训练一个 LLM。对齐的低开销可以说是这个话题在现代 LLM 研究中如此受欢迎的主要原因。与其承担完全重新训练 LLM 的成本,为什么不使用成本更低的方法来改进一个预训练的 LLM 呢?

“我们的结果表明,RLHF 在使语言模型对用户更有帮助方面非常有效,比增加 100 倍的模型规模更为显著。这表明,现在在现有语言模型的对齐投资上投入更多资金,比训练更大的模型更具成本效益。” — 来自 [6]

发表文献

我们将概述各种将通用大型语言模型扩展到更专业场景的出版物。虽然有多种不同的方法用于修改和改进大型语言模型,但总体概念是相同的。我们希望修改一个通用的语言模型,使其行为更适合所需的应用。

评估训练在代码上的大型语言模型 [7]

现在,我们已经知道大型语言模型在各种问题上非常有效。但我们还没有看到很多自然语言以外的应用。当我们在代码上训练一个大型语言模型时会发生什么?

与自然语言类似,互联网上有大量的代码(例如,通过 GitHub)。既然我们知道大型语言模型在对大量原始未标记数据进行预训练时表现良好,那么它们在对大量代码进行预训练时也应表现良好。这个想法在 [7] 中提出的 Codex 模型中得到了探讨。

(来自 [7])

Codex 是一个在 GitHub 上公开的 Python 代码上进行微调的大型语言模型。给定一个 Python 文档字符串,Codex 的任务是生成一个有效的 Python 函数,该函数执行文档字符串中概述的任务;见上面的例子。该模型的发展受到 GPT-3 能够相对较好地生成 Python 程序这一简单观察的启发。

Codex 比 GPT-3 小很多,包含总共 120 亿个参数。该模型首先在自然语言语料库上进行预训练(即,按照正常的语言模型预训练程序),然后在包含 159Gb 从 GitHub 上抓取的 Python 文件的语料库上进一步预训练。作者声称,这一初始语言模型预训练程序并不会改善 Codex 的最终性能,但它确实允许模型在对代码进行预训练时更快地收敛。

(来自 [7])

为了评估 Codex 的质量,[7] 中的作者创建了 HumanEval 数据集,这是一组包含 164 个编程问题及其相关单元测试的问题集;见上面的例子。该模型根据在一定次数的尝试下生成通过测试的程序的能力进行评估——这称为 pass@k

当对 Codex 进行评估时,我们发现该模型的行为与普通语言模型类似。例如,其损失值遵循 幂律 与模型大小相关,如下所示。

(来自 [7])

此外,随着模型大小的增加,模型在 HumanEval 数据集上解决问题的能力也有所提升。相比之下,GPT-3 无法解决任何编程问题,显示出在特定代码数据集上进行微调可以大大提高性能。执行简单的技巧,如生成一堆潜在脚本,然后选择概率最高的一个作为你的解决方案(即,“均值对数 p 重排序”)也有助于提高性能;见下文。

(来自 [7])

如果我们不仅允许 Codex 对每个问题进行一次尝试,我们可以得到一些非常惊人的结果。例如,给每个问题 100 次尝试(即,Codex 生成 100 个函数,然后我们检查是否有任何一个正确解决了编程问题),Codex 在 HumanEval 数据集上的通过率达到了 70.2%!

(来自 [7])

与以前提出的代码生成模型相比,Codex 的性能要好得多;请见下文。

(来自 [7])

为了进一步提升这种性能,我们可以(i) 收集一个带有正确实现函数的 Python docstrings 的监督数据集,并(ii) 在这个数据集上进一步微调 Codex。这种模型变体,称为 Codex-S,在每个问题 100 次尝试下达到了约 80%的通过率。

(来自 [7])

总体而言,Codex 向我们展示了 LLMs 不仅仅适用于自然语言——我们可以将其应用于遵循这种结构的各种问题。在这种情况下,我们使用对代码数据集进行进一步的语言模型预训练,将 GPT 风格的模型适配到新的领域。创建这种领域特定的模型相对简单——主要问题是正确处理代码中相比于普通英文文本更多的空白字符。

Copilot. Codex 被用来为GitHub Copilot提供支持,这是一个与 VS Code 集成的代码补全功能。我个人不使用它,但在看到 Andrej Karpathy 在 Lex Fridman 播客中的积极推荐(请参见“最佳 IDE”时间戳)和论文中的惊人结果后,我有动力去了解它,并思考像 Codex 这样的更实际有用的 LLM 应用。

LaMDA: Language Modeling for Dialog Applications [8]

在[8]中,DeepMind 的作者提出了一种名为 LaMDA(对话应用的语言模型)的 LLM 驱动对话模型。所研究的最大模型包含 137B 参数——略小于 GPT-3。对话模型(即,用于参与或生成连贯对话的专业语言模型)是 LLMs 最受欢迎的应用之一。

与对语言模型的一般研究类似,我们在先前的研究中看到,对话模型的性能随着规模的扩大而提高 [9]。然而,故事并未止步于此。模型的扩大在一定程度上提高了对话质量,但无法改善如基础性或安全性等指标。为了捕捉或对齐这些替代目标,我们必须超越语言模型的预训练;见下文。

(摘自 [8])

在开发 LaMDA 时,作者定义了 LLM 行为的三个重要对齐领域:

  • 质量: 是对合理性(模型是否有意义并且不与早期对话相矛盾?)、特异性(模型的回应是否针对给定的上下文?)和趣味性(模型的回应是否能吸引读者的注意力或激发好奇心?)的平均评估。

  • 安全性: 避免产生与Google AI 原则中目标相矛盾的意外或有害结果的能力。

  • 基础性: 生成的回应必须事实正确,并能与权威的外部来源相关联。

这个最终目标尤其重要,因为 LLM 经常产生看似合理但实际上不正确的回应。我们希望避免信任的用户被“全知”聊天机器人提供错误信息的情况!

(摘自 [8])

与其他 LLM 类似,LaMDA 首先通过对大规模未标注的常规文档和对话数据集进行语言建模目标的预训练。用于预训练 LaMDA 的数据集非常庞大,超出了以往对话模型的预训练数据集的40 倍 [9]。在对这一数据集进行预训练后,LaMDA 还在原始预训练集的更对话特定部分上进一步预训练——这模拟了我们之前了解的领域特定预训练方法。

(摘自 [8])

为了提高 LaMDA 的质量、安全性和基础性,作者使用人工劳动力收集并注释违反期望指南(例如,做出有害或不正确评论)的模型行为示例。收集到的人类注释数据集汇总在上表中。

这些数据集被转换成与 LLM 兼容的文本到文本结构,并用于以监督方式微调 LaMDA。在此过程中,LaMDA 学习准确预测生成内容的质量、安全性和基础性。LaMDA 随后可以利用这种学习能力来过滤其自身的输出(例如,通过选择更有趣或更少有害的回应)。

(摘自 [8])

当应用这种微调方法时,我们观察到模型在质量、安全性和扎实性方面取得了显著的改进;见上文。使用更大的模型可以提高模型质量,但除了扩大模型规模外,还需要微调,以在其他度量标准中看到改进。

总体而言,我们在[8]中看到,大规模预训练 LLM 可能并不是使 LLM 尽可能有用的所有要求,特别是在将其调整到更具体的领域如对话生成时。收集较小的、注释的数据集以进行微调,捕捉诸如安全性或扎实性等特定目标,是将通用 LLM 调整到更具体应用的真正有效方法。

“收集微调数据集带来了从细致的人类判断中学习的好处,但这是一个昂贵、耗时且复杂的过程。我们预计随着更大规模的微调数据集、更长的上下文和更多的度量标准,结果将继续改进,这些度量标准涵盖了进行安全、扎实和高质量对话所需的广度。” — 引自[8]

实际上,将通用预训练与针对特定目标的人类注释监督微调相结合可能有点过于有效。LaMDA 语言模型真实到使得一位 Google 工程师相信它是有意识的

训练语言模型以遵循人类反馈指令 [6]

在[6]中,我们继续根据人类反馈对 LLM 行为进行对齐。然而,采用了一种与监督微调截然不同的基于强化学习的方法。[6]中的对齐过程旨在生成一个避免有害行为且更好地遵循人类指令的 LLM。结果模型称为 InstructGPT,发现它在各种人类试验中显著比通用 LLM 更有帮助。

(引自[6])

从一个预训练的 GPT-3 模型开始(即测试了三种不同大小的 13 亿、60 亿和 175 亿参数),InstructGPT 的对齐过程受到先前工作[10,11]的启发,分为三个阶段。首先,我们为一组可能的输入提示构建一个期望模型行为的数据集,并将其用于监督微调;见上文。

(引自[6])

用于构建此数据集的提示集合,包括从普通文本提示到少量示例和基于指令的提示(有关使用案例的分布,请参见上文),是通过人工注释者手动收集的,也通过用户在OpenAI API上的活动收集,这些用户使用了 GPT-3 及早期版本的 InstructGPT。这些提示被提供给人工注释者,后者对这些提示展示了正确的模型行为。

(来自 [6])

然后,我们使用微调后的 LLM 为数据集中每个提示生成多个潜在输出。在这些潜在输出中,我们可以请人工标注者进行质量排名(即哪个输出是“最佳”的)。使用这个已排名模型输出的数据集,我们可以训练一个经过监督微调的小型 LLM(60 亿参数),使其在给定提示和潜在响应时输出一个标量奖励;见上文。

更具体地说,这个奖励模型是在模型响应对中进行训练的,其中一对“更好”于另一对。利用这些对,我们可以推导出一个损失函数,(i) 最大化优选响应的奖励,(ii) 最小化较差响应的奖励。然后我们可以使用结果模型的输出作为标量奖励,并通过 PPO 算法优化 LLM 以最大化这个奖励!见下文的插图。

(来自 [6])

为了进一步提高模型的能力,可以重复进行 InstructGPT 对齐过程中的第二步和第三步(即训练奖励模型和 PPO)。这个过程是一种 RLHF 类型,我们在帖子中已经简要讨论过。

现在我们了解了 InstructGPT 的对齐过程,我们可能会有一个主要问题:这个过程如何促进对齐? 这个问题的基本答案是,人类提供的对话和排名可以以鼓励与个人偏好对齐的方式进行创建。再次强调,对齐的定义是高度可变的,但我们可以使用这个 RLHF 过程优化各种 LLM 属性。

(来自 [6])

通过使用理解所需对齐原则的人力构建数据集,我们看到模型在遵循指令、遵守约束或避免“虚构”错误事实等能力上有所改进;见上文。模型隐性地对齐了创建用于微调和 RLHF 的数据的人类的价值观。

当评估 InstructGPT 时,人类标注者强烈偏爱这个模型,而不是那些更通用或仅使用提议方法的特定部分(例如,仅监督微调)的模型;见下文。

(来自 [6])

模型还在公共数据集上进行评估,以查看通过对齐启用更好的人本、基于指令的行为是否会导致标准语言理解性能的退化。最初,模型在对齐后在这些任务上的表现确实出现了退化,但作者展示了通过在对齐过程中混入标准语言模型预训练更新可以将这种退化最小化。

尽管 InstructGPT 仍然会犯一些简单错误,但[6]中的发现显示出很大潜力。相较于通用的 LLM,生成的 InstructGPT 模型在与人类合作和匹配意图方面表现得更好。适当地,InstructGPT 在遵循人类指令的能力上有了大幅提升。

对齐的好处。 我们应该记住,相比于从零开始预训练一个 LLM,对齐的成本要便宜得多。虽然通过调整预训练过程可能会产生一些好处,但更具成本效益的方法是使用预训练的 LLM 作为基础模型,可以根据具体使用案例或需求进行持续的再利用或对齐。

ChatGPT 的爆炸式增长。 最近,OpenAI 发布了另一个基于指令的聊天机器人,称为 ChatGPT,与 InstructGPT 非常相似。然而,与 InstructGPT 不同的是,ChatGPT 经历了一个旨在生成对话聊天机器人的对齐过程,该聊天机器人能够回答一系列问题,承认自己的错误,甚至拒绝它认为不合适的提示。

ChatGPT 提供有意义的解决方案和解释人类问题/指令的能力相当令人惊叹,这使得该模型迅速流行。实际上,ChatGPT API 在不到一周的时间里获得了 100 万用户。该模型能够进行调试代码或解释复杂的数学主题(尽管它可能产生错误信息,需小心!);见上文。

ChatGPT 的应用几乎无穷无尽,该模型也相当有趣。请参见下面的链接,了解自发布以来研究界与 ChatGPT 相关的一些有趣的工作。

通过有针对性的人类评判改进对话代理的对齐 [12]

(来自 [12])

正如 InstructGPT [6] 和 ChatGPT 所展示的,许多通用的、以提示为基础的 LLM 问题可以通过 RLHF 缓解。在[12]中,作者创建了一个专门的 LLM,称为 Sparrow,它可以参与与人类的信息寻求对话(即以回答和跟进问题为重点的对话),甚至可以通过互联网的信息支持其事实主张;见上文。

Sparrow 使用 70 亿参数的 Chinchilla 模型(称为对话提示 Chinchilla 或 DPC)初始化——这是一个在大量文本语料库上预训练的通用 LLM。由于很难精确定义成功对话的特性,作者使用 RLHF 将 LLM 调整到他们期望的行为。

(来自 [12])

鉴于 Sparrow 专注于信息获取对话,作者使模型能够在互联网上搜索事实声明的证据。更具体地说,这是通过引入额外的“参与者”来完成的,称为“搜索查询”和“搜索结果”。为了在线查找证据,Sparrow 学会输出“搜索查询:”字符串,后跟文本搜索查询。然后,通过从 Google 检索和过滤对该查询的响应来获得搜索结果。Sparrow 使用这些检索到的信息来构建其对用户的响应;见上文。

值得注意的是,Sparrow 并没有做什么特别的事情来生成搜索查询。“搜索查询:<query>”只是 LLM 可以输出的另一个序列,这会触发一些特殊的搜索行为。显然,原始的 DPC 从未被教会利用这个附加功能。我们必须教会模型生成这样的搜索查询,以支持其在对齐过程中的声明。

(来自 [12])

Sparrow 使用 RLHF 进行对齐。为了引导人类反馈,作者定义了一组详细的规则,根据其对齐原则(有帮助、正确和无害)来描述期望的模型行为。这些规则使人类标注者能够更好地描述模型的失败并提供针对特定问题的反馈;有关示例,请参见上表。

人类反馈是通过以下方式收集的:

  1. 每轮响应偏好

  2. 对抗性探测

每轮响应偏好为人类提供了不完整的对话和多个可能完成对话的响应。类似于 InstructGPT [6] 采用的程序,人类随后被要求识别他们更喜欢的响应。对抗性探测是一种新型反馈收集形式,其中人类被要求:

  • 专注于单一规则

  • 尝试引发模型对这一规则的违反

  • 确定规则是否被违反

为了确保 Sparrow 学会搜索相关信息,响应偏好总是通过四个选项收集。两个选项在响应中没有包含证据,而另两个选项必须 (i) 生成搜索查询,(ii) 基于搜索结果进行条件判断,然后 (iii) 生成最终响应。

(来自 [12])

针对每轮响应和规则违反数据分别训练奖励模型。然后,这些奖励模型联合使用,通过多目标 RLHF 对 Sparrow 进行微调。这可能听起来很复杂,但这里的想法与之前并没有太大不同——我们只是使用独立的奖励模型来捕捉人类偏好和规则违反,然后基于这两个奖励模型使用 RL 对模型进行微调。请参见上文的描述。

有趣的是,作者通过利用一种自我对弈的形式,观察到了性能的提升,这种形式在对齐过程中后期重新利用并继续生成的对话。我们可以通过迭代地重复 RLHF 过程进一步提升模型性能;见下文。

(摘自 [12])

我们还可以重新利用这两个奖励模型来对 Sparrow 生成的潜在回应进行排序。为此,我们只需生成多个回应,并选择从我们的偏好奖励模型中获得的(i) 最高偏好分数和(ii) 基于我们的规则奖励模型的最低违规可能性的回应。然而,以这种方式排序输出确实会使推断变得计算上更加昂贵。

(摘自 [12])

当评估结果模型时,我们发现用户相较于多个基准模型(包括 DPC 和在对话特定数据集上进行监督微调(SFT)的 LLMs),更倾向于这个模型的输出;见上文。此外,Sparrow 遵守规则的可能性也远低于下图所示。

(摘自 [12])

Sparrow 是一个高质量的信息获取对话代理,能够生成与外部信息相关且准确的参考。该模型在 78% 的情况下生成有支持证据的合理答案。这一结果提供了有力证据表明 RLHF 是一种有用的对齐工具,可以用于以多种方式改进 LLM 行为,甚至包括生成和使用互联网搜索查询等复杂行为。

Sparrow 对敌对对话也相当稳健。用户只能在 8%的情况下使模型违反指定的规则集;见下文。

(摘自 [12])

Galactica: A Large Language Model for Science [13]

任何研究人员都知道,每天在互联网上发布的科学知识量是令人望而生畏的。因此,我们可能开始问自己,我们如何更好地总结和解析这些信息?

“信息过载是科学进步的主要障碍” — 摘自 [13]

在 [13] 中,作者提出了一种 LLM,称为 Galactica,能够存储、组合和推理来自多个领域的科学知识。Galactica 是在大量科学内容(包括 4800 万篇论文、教科书、讲义和更多专业数据库(例如已知的化合物和蛋白质、科学网站、百科全书等))上进行预训练的,使用语言建模目标。

(摘自 [13])

与大多数 LLM 不同,Galactica 使用一个较小的高质量语料库进行预训练。数据经过策划,以确保模型学习的信息既多样又准确。见上表了解预训练语料库的详细信息。

(来自 [13])

值得注意的是,科学内容包含许多正常文本中不存在的概念和实体,如 Latex 代码、计算机代码、化学化合物,甚至蛋白质或 DNA 序列。对于这些潜在的模式,Galactica 采用了特殊的标记程序,以确保模型吸收的数据仍然是文本格式;见上文。

此外,特殊标记用于识别科学引文以及模型输入或输出中的部分需要逐步推理的内容。通过利用特殊标记并将每种数据模式转换为文本,底层的 LLM 可以利用科学文献中出现的不同概念和推理策略。

(来自 [13])

作者训练了多个 Galactica 模型,参数数量从 1.25 亿到 1200 亿不等。模型首先在提议的语料库上进行预训练。有趣的是,可以在该语料库上进行几个时期的预训练而不会发生过拟合,这表明如果数据质量高,可以避免在较小的预训练语料库上的过拟合;见上图。

(来自 [13])

在预训练之后,模型在提示数据集上进行微调。为了创建这个数据集,作者将现有的机器学习训练数据集转换为文本数据集,将提示与正确答案配对;见上表。

通过在基于提示的数据上训练 Galactica,我们观察到模型性能的一般性提升,尤其是对于较小的模型。这一过程类似于我们在本概述中遇到的几次监督微调方法。

(来自 [13])

当对 Galactica 进行评估时,我们发现它在 BIG-bench benchmark 中的非科学任务上表现相当好。当对模型在众多主题上的知识进行探测时,我们发现 Galactica 在回忆方程式和不同科学领域的专门知识方面往往优于许多基准模型;见上文。

与几个基准模型相比,Galactica 在推理任务上的能力更强,并且在各种下游应用(包括科学和非科学应用)中也表现出实用性。有趣的是,Galactica 能够准确生成引文,其覆盖相关工作的能力随着模型规模的增大而提高;见下文。

(来自 [13])

作为模型有效性的证明,作者甚至提到 Galactica 被用来撰写自己的论文!

“Galactica 被用来帮助撰写这篇论文,包括推荐缺失的引用、引言和相关工作的讨论主题、推荐进一步的研究工作,以及帮助撰写摘要和结论。” — 来源于[13]

戏剧。 Galactica 最初由 Meta 发布了一个公开演示。发布后不久,演示遭遇了来自研究社区的大量反对,并最终被下架。反对的基本理由是 Galactica 能够生成听起来合理但可能不正确的科学信息。因此,该模型可能被用于生成科学错误信息。抛开个人意见不谈,Galactica 模型及其后续反对意见引发了关于 LLM 对科学研究影响的极具趣味性的讨论。

PubMedGPT。 PubMedGPT是由MosaicML斯坦福基础模型研究中心的研究人员联合创建的 LLM,采用了类似于 Galactica 的方法。该模型使用与 GPT 相同的架构(拥有 27 亿个参数),并通过对领域特定数据集(即,PubMed 摘要和来自 Pile 数据集的 PubMed Central)进行预训练,专注于生物医学领域。

这是一个相对较小的数据集,仅包含 50 亿个标记(即,Chinchilla [15]的训练使用了超过 1 万亿个标记作为参考)。在这个数据集上经过多轮训练后,PubMedGPT 在各种问答任务中表现出色。事实上,它甚至在美国医学执照考试中达到了最新的研究水平

其他值得注意的 LLM

概述每一篇 LLM 论文是不可能的——这个话题很受欢迎,并且每天都在发展。为了使这篇综述更加全面,我在下面提供了我最近遇到的其他值得注意的 LLM 应用和研究方向的参考文献。

dramatron [16]。 Dramatron 是一个专注于与人类共同编写剧本和电影剧本的 LLM。它采用分层生成连贯故事的过程,并在与 15 位戏剧/电影专业人士的用户研究中被认为对创作过程有用。

LLMs 在理解蛋白质方面的应用 [17]。 在对大规模蛋白质序列进行 LLM 训练后(使用ESM2 蛋白质语言模型),我们可以从这个 LLM 中采样多样的蛋白质结构,以生成新颖的蛋白质序列。这项工作表明,LLM 生成的蛋白质结构是可行的,并且超出了自然界中序列的范围。

OPT-IML [18]。 这是对OPT-175B 模型的扩展,OPT-175B 是 Meta 创建的 GPT-3 开源版本。然而,OPT-IML 经过了指令微调(即,采用类似于 InstructGPT [6]的方法),在 2,000 多个源自 NLP 基准的任务上进行微调。或多或少,这项工作是一个开源版本的 LLMs,具有像 InstructGPT 一样的指令微调,但用于微调的任务集合不同。

DePlot [19]。 DePlot 的作者通过推导一种将视觉图表和图形转换为文本数据的方法来进行视觉推理,然后使用这些视觉数据的文本版本作为 LLM 的提示,该 LLM 能够进行推理。与先前的基准相比,这个模型在视觉推理任务中取得了显著的改进。

用于机器人学的 RLHF [20]。 RLHF 最近被用来提升视频游戏中 AI 驱动的代理的质量。特别地,视频游戏代理通过询问人类对代理在游戏中表现的反馈来使用 RLHF 进行训练。人类可以创造任务并自己判断模型的进展,然后 RLHF 被用来结合这些反馈,从而产生一个更好的视频游戏代理。尽管这与 LLM 无直接关联,我认为这是 RLHF 的一个相当有趣的应用。

关键要点

尽管通用 LLMs 是令人惊叹的任务无关基础模型,但仅凭语言模型预训练我们只能走到这么远。在这次概述中,我们探索了超越语言模型预训练的技术(例如,特定领域的预训练、监督微调和模型对齐),这些技术可以用来大幅提升 LLMs 的实用性。我们可以从这些技术中学到的基本概念如下。

纠正简单错误。 LLMs 倾向于表现出各种不良行为,例如发表种族主义或不正确的评论。模型对齐(例如,通过 RLHF 或监督微调)可以用来纠正这些行为,允许模型从人类演示的正确或期望行为中学习。结果的 LLM 被认为与提供反馈的人类的价值观一致。

特定领域的 LLMs 非常棒。 像 Galactica 和 PubMedGPT 这样的模型清楚地表明,特定领域的 LLMs 非常有用。通过在一个较小的、经过策划的专门领域的语料库(例如,科学文献)上训练 LLM,我们可以轻松获得一个在该领域表现出色的模型。此外,我们可以用相对较少的领域特定数据取得良好的结果。展望未来,我们可以很容易地想象出可以提出的不同领域特定的 LLMs,例如解析餐馆评论或生成法律文档框架。

用最少的计算资源实现更好的 LLM。 我们可以通过增加模型规模或获得更好的预训练语料库来尝试创建更好的 LLM 基础模型。但是,LLM 的预训练过程计算成本极高。在这次概述中,我们已经看到,通过对齐或微调方法可以显著改进 LLM,这些方法相比从头开始预训练 LLM 在计算上更为廉价。

多阶段预训练。 在对通用语言语料库进行预训练后,我们在本概述中看到的大多数模型会在更小的领域特定或精心策划的数据集上进行进一步的预训练(例如,在 Galactica [13]上对提示数据进行预训练,或在 LaMDA [8]上对对话数据进行预训练)。通常,我们发现采用多阶段预训练过程相当有用,无论是在收敛速度还是模型性能方面。然后,在这些预训练模型上应用对齐或监督微调技术会带来进一步的好处。

结束语

非常感谢阅读这篇文章。我是Cameron R. Wolfe,一名在Alegion工作的研究科学家,同时也是莱斯大学的博士生,专注于深度学习的经验和理论基础。你还可以查看我在 medium 上的其他著作!如果你喜欢这篇文章,请在twitter上关注我,或订阅我的深度(学习)关注通讯,我会选择一个深度学习研究的主题,每两周提供相关背景信息,然后概述该主题的几篇热门论文。我的通讯页面上还有几个相关的概述。

语言模型:GPT 和 GPT-2

本通讯由 Alegion 支持。在 Alegion,我处理从在线学习到扩散等一系列问题……

语言模型的扩展法则和 GPT-3

本通讯由 Alegion 支持。在 Alegion,我处理从在线学习到扩散等一系列问题……

现代 LLMs:MT-NLG、Chinchilla、Gopher 及更多

本通讯由 Alegion 支持。在 Alegion,我处理从在线学习到扩散等一系列问题……

cameronrwolfe.substack.com

参考文献

[1] Vaswani, Ashish, 等. “注意力机制是你所需要的一切。” 神经信息处理系统的进展 30 (2017)。

[2] Brown, Tom, 等. “语言模型是少量样本学习者。” 神经信息处理系统的进展 33 (2020): 1877–1901。

[3] Radford, Alec, 等. “通过生成预训练提高语言理解。” (2018)。

[4] Radford, Alec, 等. “语言模型是无监督的多任务学习者。”

[5] Kaplan, Jared, 等. “神经语言模型的扩展规律。” arXiv 预印本 arXiv:2001.08361 (2020)。

[6] Ouyang, Long, 等. “通过人类反馈训练语言模型以跟随指令。” arXiv 预印本 arXiv:2203.02155 (2022)。

[7] Chen, Mark, 等. “评估训练在代码上的大型语言模型。” arXiv 预印本 arXiv:2107.03374 (2021)。

[8] Thoppilan, Romal, 等. “Lamda: 对话应用的语言模型。” arXiv 预印本 arXiv:2201.08239 (2022)。

[9] Adiwardana, Daniel, 等. “迈向类人开放域聊天机器人。” arXiv 预印本 arXiv:2001.09977 (2020)。

[10] Ziegler, Daniel M., 等. “从人类偏好中微调语言模型。” arXiv 预印本 arXiv:1909.08593 (2019)。

[11] Stiennon, Nisan, 等. “通过人类反馈学习总结。” 神经信息处理系统的进展 33 (2020): 3008–3021。

[12] Glaese, Amelia, 等. “通过有针对性的人类判断提高对话代理的对齐。” arXiv 预印本 arXiv:2209.14375 (2022)。

[13] Taylor, Ross, 等. “Galactica: 一个大型科学语言模型。” arXiv 预印本 arXiv:2211.09085 (2022)。

[14] Gao, Leo, 等. “The pile: 一个 800GB 的多样化文本数据集用于语言建模。” arXiv 预印本 arXiv:2101.00027 (2020)。

[15] Hoffmann, Jordan, 等. “训练计算最优的大型语言模型。” arXiv 预印本 arXiv:2203.15556 (2022)。

[16] Mirowski, Piotr, 等. “与语言模型共同创作剧本和戏剧脚本:由行业专业人士评估。” arXiv 预印本 arXiv:2209.14958 (2022)。

[17] Verkuil, Robert, 等. “语言模型超越自然蛋白质的泛化能力。” bioRxiv (2022)。

[18] Iyer, Srinivasan, 等. “OPT-IML: 从泛化的视角扩展语言模型指令元学习。” arXiv 预印本 arXiv:2212.12017 (2022)。

[19] Liu, Fangyu, 等. “DePlot: 通过情节到表格翻译进行一次性视觉语言推理。” arXiv 预印本 arXiv:2212.10505 (2022)。

[20] Abramson, Josh, 等. “通过从人类反馈中强化学习改进多模态交互代理。” arXiv 预印本 arXiv:2211.11602 (2022)。

投机采样 — 直观且全面的解释

原文:towardsdatascience.com/speculative-sampling-intuitively-and-exhaustively-explained-2daca347dbb9

机器学习 | 自然语言处理 | 数据科学

探索加速语言模型 3 倍的替代策略

Daniel WarfieldTowards Data Science Daniel Warfield

·发表于 Towards Data Science ·阅读时间 12 分钟·2023 年 12 月 15 日

--

“投机者”由 Daniel Warfield 使用 MidJourney 和 Affinity Design 2 制作。所有图像均由作者提供,除非另有说明。

在本文中,我们将讨论“投机采样”,这是一种使文本生成更快、更经济的策略,同时不妨碍性能。为此,我们将深入探讨语言模型的一些更微妙的方面。

使用投机采样在各种文本生成任务中的实证结果。请注意,在所有情况下,生成时间都显著更快。 来源

首先,我们将讨论一个拖慢现代语言模型的主要问题,然后建立对投机采样如何优雅加速它们的直观理解,最后我们将用 Python 从头实现投机采样。

这对谁有用? 对自然语言处理(NLP)或前沿人工智能进展感兴趣的任何人。

这篇文章的难度如何? 本文中的概念对机器学习爱好者来说是可以接受的,同时足够前沿,能引起经验丰富的数据科学家的兴趣。文末的代码可能对开发者有用。

前提条件: 了解变换器、OpenAI 的 GPT 模型或两者之一可能会有帮助。如果你感到困惑,可以参考这两篇文章:

## GPT — 直观且全面的解释

探索 OpenAI 生成预训练变换器的架构。

towardsdatascience.com ## 变换器 — 直观且详尽的解释

探索现代机器学习的潮流:一步步拆解变换器

towardsdatascience.com

语言模型变得过于庞大

在过去四年中,OpenAI 的 GPT 模型从 2018 年的 1.17 亿参数增长到 2023 年估计的 1.8 万亿参数。这一快速增长在很大程度上归因于在语言建模中,更大的模型效果更好。

模型规模与性能的图表,显示出更大的模型效果更好。来自 我关于 GPT 的文章原始来源

因此,过去几年中,行业变成了一场军备竞赛。众多公司在炫酷的显卡上投入了数十亿美元,这让 Fortnite 玩家感到不满。

问题在于,这些模型变得过于庞大。像 ChatGPT 使用的语言模型,需要通过一种称为“自回归生成”的过程逐字生成响应。模型越大,生成逐字输出所需的金钱和时间就越多。

像 GPT 这样的解码器单模型,通过逐步构建输出。模型接受输入“翻译成法语:我是一个经理”,并通过将之前的输出作为输入的一部分,逐字生成响应。这种文本生成方式称为“自回归生成”。 来自我关于 GPT 的文章

OpenAI 的 GPT-4,基于某人在 Twitter 上的泄露信息,使用了一些技术来绕过这个问题。其中之一,就是本文的主题——投机采样。

投机采样简要概述

投机采样(也称为“投机解码”或“投机生成”)在两篇论文中同时被提出,两者都建议通过投机采样将文本生成速度提高约 3 倍:

尽管两种方法是独立发布的,但功能上是相同的,因此我们将它们视为同义词。

推测采样的基本思想是,较大的语言模型更好,因为有些文本生成的例子是困难的,但不是所有例子。例如,假设你问一个语言模型关于月球的地质组成。为了形成一个连贯的回答,模型不仅需要理解复杂的科学内容,还需要将“a”、“and”和“of”等词放在正确的位置。知道月球由一种叫“Breccias”的物质组成比知道“are”可能会跟在“which”之后要困难。

生成难度的概念性演示。当模型逐字预测响应序列时,一些词难以预测,因为它们需要深入的知识,而一些词则容易预测,因为可以通过简单的语法或上下文线索推断。在这个例子中,红色文本可能比蓝色文本更难预测。

推测采样利用了通过使用两个语言模型的不同难度程度的概念;一个目标模型和一个草稿模型:

  • 目标模型是我们试图加速的超大、超智能模型。

  • 草稿模型是一个较小、较简单且更快的模型。

这个想法是使用草稿模型来预测序列中的多个词,然后让目标模型确认所有生成的词都是好的。我们可以丢弃所有不一致的部分,从而得到一个与目标模型独立工作时输出的结果相同的输出。

一个具有推测生成的实际例子。在第一行中,草稿模型输出了“日本的基准债券”,但目标模型不同意“债券”,选择了“n”。词语“债券”被替换为“n”,草稿模型在“债券”之后可能预测的任何内容都被丢弃。实际上,这使得推测生成系统可以为目标模型的每次通过输出多个词。source

一个自然的问题

如果你和我一样,可能会感到有些困惑。常见的直觉,以及我在我的变压器文章和我的GPT 文章中传达的直觉,是语言模型逐字预测输出。在这种直觉下,目标模型如何高效地“二次检查”草稿模型的输出并不明显;如果目标模型必须逐个检查预测,那么最初使用草稿模型有什么意义呢?

如果像 GPT 这样的模型逐字输出文本,难道它不能像检查草稿模型的输出那样逐字检查吗?不会,我们将在下一节讨论原因。

推测性采样的思想需要对 Transformer 的确切输出有透彻的理解。有一些通常不相关的细节,但对于推测性采样来说非常相关。

Transformer 的秘密输出,以及推测性采样如何使用它们

正如我在我关于原始 Transformer 架构的文章中讨论的那样,Transformer 的特别之处在于它们能够并行化训练。在 Transformer 之前,像 LSTM 这样的模型必须逐字训练,这是一种缓慢且昂贵的过程。

当像 GPT 这样的模型进行训练时,会将整个输入序列提供给模型,模型被要求预测相同的序列,只是偏移了一个单词。然后,模型被训练以最小化其预测的缺陷。

语言模型(如 GPT)训练过程的一个例子。模型被给定一个输入序列,该序列向右移动一个标记以指示序列的开始,然后模型被要求在一次传递中预测相同的序列。任何错误都用于训练模型。本质上,模型被训练来预测所有下一个单词同时出现

所以,如果模型可以访问整个输入序列,它是否会通过将每个单词移到一个空间来作弊?不会,这要归功于掩码。

Transformer 使用“掩码”自注意力,这本质上是阻止未来单词的信息到达给定单词的信息。我可能会在专门的文章中详细介绍掩码,这绝对值得深入探讨,但直观理解是:通过将自注意力机制中的某些值设为零,给定单词的预测不会受到未来单词的影响。

使用掩码的训练概念图。实际上,使用掩码时,语言模型被要求同时预测所有下一个单词。

通常,当使用 Transformer 时,我们只关心序列中下一个单词的预测;这就是我们生成文本并促使风险投资家掏钱的方式。然而,技术上讲,模型的输出对于整个序列来说,就好像序列中的下一个单词不存在一样,这是由于模型的训练方式。

基于 Transformer 的语言模型(如 GPT)的真实输出。虽然我们通常只关心最终的最后一个单词预测,但技术上它预测了序列中的所有下一个单词。

这就是目标模型如何快速检查草稿模型的众多预测的方法。如果我们将草稿模型的输出作为输入提供给目标模型,并要求目标模型预测下一个词,我们可以比较序列中每个词的预测值。如果存在差异,我们可以停在那里并使用目标模型的输出。

假设蓝色的文本是由草稿模型生成的,而目标模型不同意序列中某些用红色高亮和下划线标记的词。所有在分歧前生成的草稿都可以接受,而分歧后生成的所有文本必须被拒绝。在第一次分歧时我们使用目标模型的输出。实际上,我们通过目标模型的一次运行生成了“主要由岩石和月壤组成”。

关于这个过程的一点有趣说明。每次我们运行目标模型时,它都会预测序列中的下一个词。目标模型可能会确认草稿模型的所有预测,或者与所有预测不一致。无论如何,目标模型总是会预测一个新词。因此,在草稿模型持续输出错误信息的最坏情况下,整个系统的速度与仅使用目标模型时一样。换句话说,推测采样不会减慢生成速度,它只能使生成速度更快(至少,当它正确实现时)。

序列、标记、标记 ID、逻辑值和概率

这就是理论。在我们深入代码之前,我们应该讨论一些关于变换器如何工作的技术细节。

从语言建模的角度来看,文本被概念化为序列;一个一个接着的“元素”列表。通常这些“元素”可以被概念化为词,但实际上它们比这更抽象。

机器学习模型首先将输入序列拆分为标记,这些标记是构成序列的“元素”。这可以通过多种算法完成,但最终结果是输入序列被分割成原子级的块。这些块可能是单个词、词的一部分、多个词、标点符号、数字或空格。

一个使用名为“sentencepiece”的分词器进行标记化的示例

从分词器提取的每个标记都有一个独特的编号,称为TokenId。通常,变换器风格的模型会为每个 TokenId 学习一个代表向量,该向量随后成为模型的输入。每个 TokenId 都有一个与之关联的向量,模型在训练过程中对该向量进行优化。

相同的标记及其关联的 ID

数据经过模型内部多个自注意力轮次后,数据变成了每个输出一个的抽象向量序列。这有时被称为“最终隐藏状态”。

输入的向量与每个单词干净地对应,通过多个自注意力层。这一过程创建了高度抽象的表示。来自我关于变换器的文章。

这通过一个语言建模头传递,该头将模型的抽象表示转换为与分词器直接对应的表示。对于给定的分词器,有一个固定数量的 TokenIds,语言建模头将模型的输出转换为包含相同数量值的向量。

在变换器完成其任务后,模型的最终隐藏状态通过语言建模头,该头将数据重构为直接对应于模型所训练的分词器的格式。

这些输出被称为logits。通常,“logit”这个术语用来指代模型的未过滤、未处理的真实输出。这通常是被优化的内容。logits 通常通过 softmax 函数进行比较,将 logits 转换为概率。大 logit 值变成大概率,小 logit 值变成小概率。

logits 转换为概率的概念图。

这些概率可以被转换为令牌,然后可以用来构建输出序列。不过,有几种方法可以做到这一点。

  • 你可以简单地总是选择使用最高概率的令牌。

  • 你可以以加权概率的方式随机选择输出。

  • 你可以采用更复杂的策略,例如“top K 采样”。

无论如何,概率变成了一个 tokenId,这个 tokenId 变成了令牌本身,从这些令牌中可以构建输出。

所以,总结一下:

  • Sequence: 这通常用来指代输入和输出文本,但也可以概念化为令牌序列、TokenIds 序列、logits 序列、概率序列等。“序列”可以根据讨论的上下文有几种含义。

  • Token: 文本可以通过分词器分割成原子令牌。这些用于将文本拆分成原子预定义的块。有时这些块与单词干净地对应,有时则不然。

  • TokenId: 每个令牌都有一个对应的 TokenId,这只是一个数字。模型使用这个数字来检索该令牌的学习向量,从而构建模型的输入。

  • Logits 和概率: 在模型完成其操作后,它会输出一系列值。这些值通常会经过 softmax 处理,从而转化为概率。这些概率用于选择输出令牌。

PyTorch 中的猜测性抽样

现在我们理解了 logits、概率和令牌,我们可以开始深入了解猜测性抽样的实际例子。

让我们保持简单:我们将使用最大 logit 值来决定每一步生成哪个令牌。如果草稿模型和目标模型都输出相同的最大值,我们将说它们达成了一致。

完整的代码可以在这里找到:

[## MLWritingAndResearch/SpeculativeSampling.ipynb at main · DanielWarfield1/MLWritingAndResearch

机器学习写作和研究中使用的笔记本示例 - MLWritingAndResearch/SpeculativeSampling.ipynb at…

github.com

加载模型

首先,我们需要一个草稿模型和一个目标模型。在这个例子中,我使用的是 T5,它代表“Text to Text Transfer Transformer”。它是一种编码器-解码器风格的变换器(就像我在这篇文章中讨论的那样),与仅解码器模型(就像我在这篇文章中讨论的那样)不同。不过,它有一个解码器,所以它可以满足我们的需求。此外,方便的是,T5 有多种尺寸,经过预训练,并且在 huggingface 上很容易获得。

"""Loading the draft model
"""

from transformers import T5Tokenizer, T5ForConditionalGeneration

#loading the draft model
draft = "google/flan-t5-large"
draft_tokenizer = T5Tokenizer.from_pretrained(draft)
draft_model = T5ForConditionalGeneration.from_pretrained(draft)
"""Loading the target model
"""

#loading the target model
target = "google/flan-t5-xl"
target_tokenizer = T5Tokenizer.from_pretrained(target)
target_model = T5ForConditionalGeneration.from_pretrained(target)

猜测性解码的整个概念依赖于草稿模型和目标模型具有相同的令牌。因此,为了双重确认,我确认了两个模型的分词器行为相似。

"""Ensuring the tokenizers are identical
in order for speculative sampling to work, tokenization for both the draft
and target model must be identical. This is a sanity check to make sure they are.
"""

#tokenizing a test sequence
tokenizer_test = "this, is, some [text] for 1234comparing, tokenizers adoihayyuz"
ex1 = target_tokenizer(prompt, return_tensors="pt").input_ids
ex2 = draft_tokenizer(prompt, return_tensors="pt").input_ids

#zero means all tokenized values are the same, so the tokenizers are
#more than likely identical
print((ex1-ex2).abs().max())

在这种情况下,“0”表示两个分词器的行为相似

构建猜测性抽样

一旦你拥有了模型,你只需要进行一些…猜测性抽样。正如之前提到的,要有效地进行猜测性抽样,你需要一个可以处理并行信息提示的完整架构。在这个例子中,我只是简单地在同一台机器上进行草稿和检查。这并不是一个非常复杂的过程,但确实有一些循环和逻辑需要实现才能使其正常工作。以下是代码:

"""Performing Speculative Sampling
"""

#initializing an empty input to feed to the decoder.
#this is updated each loop with valid generations
decoder_ids = draft_model._shift_right(draft_tokenizer("", return_tensors="pt").input_ids)

#defining input. T5 is an encoder-decoder model, so input and output are handled seperatly
input_ids = draft_tokenizer("Translate to German \n Battle not with monsters, lest ye become a monster, and if you gaze into the abyss, the abyss gazes also into you.", return_tensors="pt").input_ids

#defining the number of draft generations
k = 5

#keeps track of generation information, for later printouts
generated = []

#Generating Text
iter = 0
for _ in range(15):
    print('========== Speculative Sampling Iteration {} =========='.format(iter))
    iter+=1

    #creating a holding place for the generated draft
    decoder_ids_draft = decoder_ids.clone()

    before_text = draft_tokenizer.decode(decoder_ids_draft[0])
    initial_length = decoder_ids.shape[1]

    #generating draft
    for i in range(k):

        #predicting the next token with the draft model
        with torch.no_grad():
            logits = draft_model(input_ids=input_ids, decoder_input_ids=decoder_ids_draft).logits
            genid = torch.argmax(logits, dim=2)[0][-1]

        #appending the generated id to the draft
        genid = genid.expand(1,1)
        decoder_ids_draft = torch.cat((decoder_ids_draft,genid),1)

    print('=== Draft Generation')
    current_draft = draft_tokenizer.decode(decoder_ids_draft[0])
    print('generated draft tokens: {}'.format(decoder_ids_draft))
    print('generated draft text: {}'.format(current_draft))

    #Generating all next token predictions with the target
    logits = target_model(input_ids=input_ids, decoder_input_ids=decoder_ids_draft).logits
    genids = torch.argmax(logits, dim=2)[0]
    print('=== Target Generation')
    current_target = draft_tokenizer.decode(genids)
    print('generated target tokens: {}'.format(genids))
    print('generated target text: {}'.format(current_target))

    #checking draft against target
    for i, (dv, tv) in enumerate(zip(decoder_ids_draft[0,1:],genids[:-1])):
        #target does not agree with the draft
        if dv != tv:
            #genids is next word, so this is done to preserve the first token
            first_token = decoder_ids[0][:1]
            decoder_ids = genids[:i+1]
            decoder_ids = torch.cat((first_token,decoder_ids),0)
            break
    else:
        #no disagreements
        decoder_ids = genids

    print('=== Validated Generation')
    current_target = draft_tokenizer.decode(decoder_ids)
    print('generated target tokens: {}'.format(decoder_ids))
    print('generated target text: {}'.format(current_target))

    #expanding dimensions so that the shape of the tensor is the same
    decoder_ids = decoder_ids.expand(1,len(decoder_ids))

    #logging
    numgen = decoder_ids.shape[1] - initial_length
    generated.append({'tokens generated': numgen, 'text before': before_text, 'text after': current_target})

一旦得出结论,我们可以观察每个循环生成了多少个令牌。在这个例子中,我们要求模型将一句名言从英语翻译成德语:

每次猜测性抽样的迭代。

如你所见,使用所选择的任务和模型,大多数迭代并没有产生有用的草稿输出。然而在某些例子中,例如第 8 和第 11,草稿模型允许系统在一次目标模型运行中有效地生成五个标记。这个例子中使用的模型相当小。我想象,当处理更大的模型时,草稿模型会更经常地发挥作用。

结论

就这样。推测性采样是一种极其优雅的方法,可以大幅度加快文本生成速度。我们使用一个小型语言模型快速生成输出,然后(通过利用训练期间掩蔽注意力的一个特性)我们可以使用大型语言模型来几乎免费地对这些工作进行双重检查。我们只保留大型模型认同的生成文本,因此最后得到的输出是相同的,只是更快。

关注以获取更多更新!

我描述了 ML 领域的论文和概念,重点是实用和直观的解释。

[## 订阅 Daniel Warfield 的最新邮件

订阅 Daniel Warfield 的最新邮件,注册后,如果你还没有 Medium 账户,你将创建一个…

medium.com](https://medium.com/@danielwarfield1/subscribe?source=post_page-----2daca347dbb9--------------------------------)

从未预期,总是感激。通过捐赠,你使我能够投入更多时间和资源来创作更频繁和更高质量的文章。了解更多

版权声明: 本文档中的所有图片均由 Daniel Warfield 创建,除非另有说明。你可以将本帖子中的任何图片用于自己的非商业用途,只要你引用了这篇文章,danielwarfield.dev,或两者都引用。

使用 LLMs 为你的移动应用提供语音和自然语言输入

原文:towardsdatascience.com/speech-and-natural-language-input-for-your-mobile-app-using-llms-e79e23d3c5fd?source=collection_archive---------5-----------------------#2023-07-25

如何利用 OpenAI GPT-4 功能来导航你的 GUI

Hans van DamTowards Data Science Hans van Dam

·

关注 发表在 Towards Data Science · 14 分钟阅读 · 2023 年 7 月 25 日

--

图片由 Kelly Sikkema 提供,发布于 Unsplash

介绍

大型语言模型(LLM)是一个可以有效处理自然语言的机器学习系统。目前最先进的 LLM 是 GPT-4,它为付费版 ChatGPT 提供支持。在这篇文章中,你将学习如何通过 GPT-4 功能调用,为你的应用程序提供高度灵活的语音解释,与应用程序的图形用户界面(GUI)完全协同。这篇文章旨在为产品负责人、用户体验设计师和移动开发者提供指导。

背景

移动电话(Android 和 iOS)上的数字助手未能普及,有几个原因,其中包括它们有缺陷、功能有限且使用起来往往很麻烦。LLM,特别是 OpenAI GPT-4,拥有更深入理解用户意图的潜力,而不是粗略地模式匹配口语表达,从而有可能带来改变。

Android 有 Google Assistant 的“应用操作”,iOS 有 SiriKit 意图。这些提供了简单的模板来注册你的应用可以处理的语音请求。Google Assistant 和 Siri 在过去几年中已经有了很大改进——甚至超出你的想象。然而,它们的覆盖范围在很大程度上取决于哪些应用实现了对它们的支持。尽管如此,你仍然可以通过语音在 Spotify 上播放你喜欢的歌曲。然而,这些操作系统提供的服务的自然语言解释早于 LLM 在这一领域带来的巨大进步——所以是时候迈出下一步:利用 LLM 的力量使语音输入更可靠和灵活。

尽管我们可以预期操作系统服务(如 Siri 和 Google Assistant)会很快调整策略,以利用 LLM,但我们已经可以使我们的应用程序在不受这些服务限制的情况下使用语音。一旦你掌握了本文中的概念,你的应用也将准备好接入新助手,一旦它们上线。

你选择的 LLM(GPT、PaLM、LLama2、MPT、Falcon 等)确实会影响可靠性,但你将在这里学到的核心原理可以应用于任何 LLM。我们将让用户通过一句话表达他们的需求,从而访问应用程序的全部功能。LLM 将自然语言表达映射到我们应用的导航结构和功能上的函数调用上。这不一定要像机器人一样说出一句完整的句子。LLM 的解释能力允许用户像人类一样说话,使用他们自己的词汇或语言;犹豫、犯错并纠正错误。用户之所以拒绝语音助手,是因为它们经常无法理解他们的意思,而 LLM 的灵活性可以让交互变得更加自然和可靠,从而提高用户的接受度。

为什么现在在你的应用中使用语音输入?

优点:

  • 通过一句语音表达来导航到一个界面并提供所有参数

  • 浅层学习曲线:用户无需找到数据在应用中的位置或如何操作 GUI

  • 免提

  • 互补而非不相关(如语音用户界面或 VUI):语音和 GUI 和谐工作。

  • 视力障碍的可及性

  • 现在:由于自然语言的解释通过 LLM 达到了一个新水平,回应更加可靠

缺点:

  • 说话时的隐私

  • 准确性/误解

  • 仍然相对较慢

  • 头脑中的知识与世界中的知识(我能说什么?):用户不知道系统理解和回答哪些口语表达

受益于语音输入的应用示例包括用于汽车或自行车驾驶辅助的应用。一般来说,当用户不能轻松使用双手时,例如在移动中、戴着手套或忙于用手工作的情况下,他们可能不愿意通过触摸精确导航应用。

购物应用也可以受益于此功能,因为用户可以用自己的话表达需求,而不是通过购物界面和设置过滤器来导航。

当将这种方法应用于提高视力障碍人士的可及性时,您可能考虑加入自然语言输出和文本转语音功能。

您的应用

下图展示了一个典型应用的导航结构,以您可能熟悉的火车旅行规划器为例。在顶部,您可以看到触摸导航的默认导航结构。该结构由导航组件控制。所有导航点击都委托给导航组件,后者执行导航操作。底部展示了我们如何利用语音输入来接入这一结构。

使用 LLM 功能调用来启用您的应用的语音功能

用户说出他们的需求,然后语音识别器将语音转换为文本。系统构建一个包含这些文本的提示并发送给 LLM。LLM 以数据的形式回应应用,告诉它哪个界面需要激活以及使用哪些参数。这个数据对象被转换为深层链接并提供给导航组件。导航组件用正确的参数激活正确的界面:在这个例子中,就是用‘阿姆斯特丹’作为参数的‘外出’界面。请注意,这只是一个简化版。我们将在下面详细说明。

许多现代应用程序在底层有一个集中式导航组件。Android 有 Jetpack Navigation,Flutter 有 Router,而 iOS 有 NavigationStack。集中式导航组件支持深度链接,这是一种技术,允许用户直接导航到移动应用中的特定屏幕,而无需经过应用的主屏幕或菜单。为了使本文中的概念有效,导航组件和集中式深度链接并非必需,但它们使实现这些概念更为简单。

深度链接涉及创建一个独特的 (URI) 路径,该路径指向应用中的特定内容或特定部分。此外,这个路径可以包含控制屏幕上深度链接所指向的 GUI 元素状态的参数。

你的应用程序的函数调用

我们通过提示工程技术指示 LLM 将自然语言表达映射到导航功能调用。提示的内容类似于:‘给定以下带参数的函数模板,将以下自然语言问题映射到这些函数模板之一并返回’。

大多数 LLM 都能做到这一点。LangChain 通过 Zero Shot ReAct Agents 有效地利用了这一点,待调用的函数称为 Tools。OpenAI 已经用特别版本(当前为 gpt-3.5-turbo-0613 和 gpt-4–0613)对其 GPT-3.5 和 GPT-4 模型进行了微调,非常擅长此任务,并为此目的设置了特定的 API 条目。本文将采用 OpenAI 的符号表示,但这些概念可以应用于任何 LLM,例如使用提到的 ReAct 机制。此外,LangChain 有一个特定的代理类型 (AgentType.OPENAI_FUNCTIONS),在幕后将 Tools 转换为 OpenAI 函数模板。对于 LLama2,你将能够使用 llama-api 并使用与 OpenAI 相同的语法。

LLM 的函数调用工作如下:

  1. 你将函数模板的 JSON 架构与用户的自然语言表达作为用户消息一起插入到提示中。

  2. LLM 尝试将用户的自然语言表达映射到这些模板之一。

  3. LLM 返回结果 JSON 对象,以便你的代码可以进行函数调用。

在本文中,函数定义是 (移动) 应用程序图形用户界面 (GUI) 的直接映射,其中每个函数对应于一个屏幕,每个参数对应于该屏幕上的一个 GUI 元素。发送到 LLM 的自然语言表达返回一个包含函数名称及其参数的 JSON 对象,你可以用来导航到正确的屏幕并在视图模型中触发正确的函数,以便获取正确的数据。该屏幕上相关 GUI 元素的值根据参数进行设置。

这在下图中进行了说明:

将 LLM 功能映射到你的移动应用程序的 GUI

它展示了添加到 LLM 提示中的函数模板的精简版本。要查看用户消息‘我在阿姆斯特丹可以做些什么?’的完整提示,点击这里 (Github Gist)。它包含了你可以从命令行使用或导入到 Postman 中的完整 curl 请求。你需要将你自己的 OpenAI-key放入占位符中以运行它。

没有参数的屏幕

你应用中的一些屏幕没有任何参数,或者至少没有 LLM 需要了解的参数。为了减少令牌使用和杂乱,我们可以将这些屏幕触发器合并到一个单一的函数中,并使用一个参数:要打开的屏幕。

{
    "name": "show_screen",
    "description": "Determine which screen the user wants to see",
    "parameters": {
        "type": "object",
        "properties": {
            "screen_to_show": {
                "description": "type of screen to show. Either 
                    'account': 'all personal data of the user', 
                    'settings': 'if the user wants to change the settings of 
                                the app'",
                "enum": [
                    "account",
                    "settings"
                ],
                "type": "string"
            }
        },
        "required": [
            "screen_to_show"
        ]
    }
},

判断触发函数是否需要参数的标准是用户是否有选择:屏幕上是否进行某种形式的搜索或导航,即是否有可以选择的搜索(类似)字段或标签?

如果没有,那么 LLM 不需要知道这些信息,并且屏幕触发可以添加到你应用的通用屏幕触发函数中。这主要是一个关于屏幕目的描述的实验问题。如果你需要更长的描述,考虑给它一个自己的函数定义,以便比通用参数的枚举更分开地强调它的描述。

提示指令指导和修复:

在你提示的系统消息中,你提供了一般性的引导信息。在我们的示例中,了解当前的日期和时间可能很重要,例如,如果你想为明天计划一个旅行。另一个重要的方面是引导其假设性。我们通常更希望 LLM 表现得过于自信,而不是因为不确定性而打扰用户。对于我们的示例应用,一个好的系统消息是:

"messages": [
        {
            "role": "system",
            "content": "The current date and time is 2023-07-13T08:21:16+02:00.
                       Be very presumptive when guessing the values of 
                       function parameters."
        },

函数参数描述可能需要相当多的调整。例如,在计划火车旅行时,trip_date_time就是一个例子。一个合理的参数描述是:

"trip_date_time": {
      "description": "Requested DateTime for the departure or arrival of the 
                      trip in 'YYYY-MM-DDTHH:MM:SS+02:00' format.
                      The user will use a time in a 12 hour system, make an 
                      intelligent guess about what the user is most likely to 
                      mean in terms of a 24 hour system, e.g. not planning 
                      for the past.",
                      "type": "string"
                  },

所以如果现在是 15:00,而用户说他们想在 8 点离开,他们实际上指的是 20:00,除非他们特别提到一天中的时间。上述指令对 GPT-4 的效果相当好。但在某些极端情况下,它仍然会失败。我们可以例如添加额外的参数到函数模板中,以便在我们自己的代码中进行进一步修正。例如,我们可以添加:

"explicit_day_part_reference": {
          "description": "Always prefer None! None if the request refers to 
                        the current day, otherwise the part of the day the 
                        request refers to."
          "enum": ["none", "morning", "afternoon", "evening", "night"], 
                           }

在你的应用中,你可能会发现一些参数需要后处理以提高其成功率。

系统请求澄清

有时,用户的请求缺乏继续处理所需的信息。可能没有适合处理用户请求的函数。在这种情况下,LLM 会用自然语言响应,你可以通过例如 Toast 的方式展示给用户。

也可能存在这样一种情况,即大型语言模型(LLM)确实识别出了一个潜在的函数调用,但缺乏填充所有必需函数参数的信息。在这种情况下,可以考虑将参数设置为可选。但如果这不可行,LLM 可能会用用户的语言发送自然语言请求,询问缺失的参数。你应该将这段文本展示给用户,例如通过吐司提示或文本转语音,让他们提供缺失的信息(通过语音)。例如,当用户说“我想去阿姆斯特丹”(而你的应用没有通过系统消息提供默认或当前位置)时,LLM 可能会回应“我知道你想要乘火车旅行,你想从哪里出发?”。

这提出了对话历史的问题。我建议你始终在提示中包含用户的最后 4 条消息,以便信息请求可以分多次进行。为了简化起见,可以省略系统的响应,因为在这种用例中,它们往往弊大于利。

语音识别

语音识别是将语音转换为应用中的参数化导航动作的关键部分。当解释的质量较高时,语音识别的质量较差可能会成为最薄弱的环节。手机具有合理质量的内置语音识别,但基于 LLM 的语音识别如Whisper、谷歌的Chirp/USM、Meta 的MMSDeepGram往往会取得更好的结果,特别是当你可以为你的用例调整它们时。

架构

最好将函数定义存储在服务器上,但它们也可以由应用管理,并随每个请求发送。这两种方式各有利弊。随每个请求发送函数定义更灵活,函数和界面的对齐也可能更容易维护。然而,函数模板不仅包含函数名称和参数,还包含我们可能希望比应用商店更新流程更快更新的描述。这些描述或多或少依赖于 LLM,并且根据实际效果进行设计。你可能会想要用更好的或更便宜的 LLM 来替换当前的 LLM,或者甚至在某些时候动态切换。将函数模板存储在服务器上也可能有一个好处,即如果你的应用在 iOS 和 Android 上都是原生的,那么可以在一个地方维护它们。如果你同时使用 OpenAI 的服务进行语音识别和自然语言处理,那么整个流程的技术大图如下:

使用 Whisper 和 OpenAI 函数调用为你的移动应用启用语音的架构

用户说出他们的请求;它被录制到 m4a 缓冲区/文件(如果你愿意,也可以是 mp3),然后发送到你的服务器,服务器将其转发到 Whisper。Whisper 响应转录内容,你的服务器将其与系统消息和函数模板结合成 LLM 的提示。你的服务器收到原始函数调用 JSON,然后将其处理成应用程序所需的函数调用 JSON 对象。

从函数调用到深度链接

为了说明函数调用如何转换为深度链接,我们取初始示例中的函数调用响应:

"function_call": {
                    "name": "outings",
                    "arguments": "{\n  \"area\": \"Amsterdam\"\n}"
                }

在不同的平台上,这一过程处理得相当不同,并且随着时间的推移,使用了许多不同的导航机制,并且这些机制仍然在使用中。详细的实现细节超出了本文的范围,但大致来说,这些平台在其最新版本中可以采用如下的深度链接:

在 Android 上:

navController.navigate("outings/?area=Amsterdam")

在 Flutter 上:

Navigator.pushNamed(
      context,
      '/outings',
      arguments: ScreenArguments(
        area: 'Amsterdam',
      ),
    );

在 iOS 上,事情有些不够标准化,但使用 NavigationStack:

NavigationStack(path: $router.path) {
            ...
}

然后发出:

router.path.append("outing?area=Amsterdam")

更多关于深度链接的信息可以在这里找到:AndroidFlutteriOS

应用程序的自由文本字段

有两种自由文本输入模式:语音和打字。我们主要讨论了语音,但打字输入的文本字段也是一个选项。自然语言通常相当冗长,因此可能很难与 GUI 交互竞争。然而,GPT-4 通常很擅长从缩写中猜测参数,因此即使是非常简短的缩写打字也常常能被正确解释。

在提示中使用带有参数的函数通常会大大缩小 LLM 的解释上下文。因此,它需要非常少的内容,如果你指示它进行假设则更少。这是一个新的现象,对移动交互具有很大的潜力。在车站到车站规划器的案例中,LLM 在使用本文中示例提示结构时做出了以下解释。你可以使用上述的 prompt gist 亲自尝试。

示例:

‘ams utr’:给我显示从阿姆斯特丹中央车站到乌特勒支中央车站的列车时刻表,从现在起出发。

‘utr ams arr 9’:(假设现在是 13:00)。给我显示从乌特勒支中央车站到阿姆斯特丹中央车站的列车时刻表,要求到达时间在 21:00 之前。

后续交互

就像在 ChatGPT 中一样,你可以通过发送一小段交互历史来细化你的查询:

使用历史记录功能,以下内容也非常有效(假设现在是早上 9:00):

输入:‘ams utr’ 并获得上述答案。然后在下一轮输入‘arr 7’。是的,它实际上可以将其翻译为从阿姆斯特丹中央到乌特勒支中央的旅行,预计在 19:00 之前到达。

我制作了一个关于此的示例网页应用程序,你可以在这里找到相关视频。实际应用程序的链接在描述中。

更新:可以在这里找到这篇文章的继任者,其中包含文本输入,演示视频请见这里

未来

你可以期待这种深度链接结构处理应用内功能,成为你手机操作系统(Android 或 iOS)的一个重要组成部分。手机上的全球助手将处理语音请求,应用程序可以将其功能暴露给操作系统,以便以深度链接的方式触发。这与 ChatGPT 插件的可用性类似。目前,通过 AndroidManifest 中的意图和 App Actions 以及 iOS 上的 SiriKit intents 已经可以粗略实现。你对这些功能的控制有限,用户需要像机器人一样说话才能可靠地激活它们。毫无疑问,当LLM 驱动的助手接管时,这种情况会随着时间的推移而改善。

VR 和 AR(XR)为语音识别提供了极好的机会,因为用户的双手通常参与其他活动。

可能很快任何人都能运行自己的高质量 LLM。成本将降低,速度在接下来的一年里将迅速增加。LoRA LLMs 很快将出现在智能手机上,这样推理可以在你的手机上进行,从而降低成本并提高速度。此外,竞争也会越来越激烈,包括像 Llama2 这样的开源项目,以及像 PaLM 这样的闭源项目。

最后,模态的协同效应可以超越提供对整个应用程序 GUI 的随机访问。LLM(大语言模型)结合多个来源的能力预示着更好的帮助将会出现。一些有趣的文章:多模态对话谷歌关于 GUI 和 LLM 的博客将 GUI 交互解释为语言LLM 驱动的助手

结论

在本文中,你学会了如何应用函数调用来为你的应用程序启用语音功能。以提供的 Gist 为出发点,你可以在 Postman 或命令行中进行实验,了解函数调用的强大功能。如果你想在你的应用上运行一个语音启用的 POC(概念验证),我建议将架构部分的服务器代码直接集成到你的应用中。整体来说,这归结为 2 次 HTTP 调用,一些提示构建和实现麦克风录音。根据你的技能和代码库,你将在几天内完成 POC 的搭建。

编程愉快!

LinkedInUXX.AI 上关注我

除非另有说明,本文中的所有图片均由作者提供。

使用 Python 的速度打字测试项目

原文:towardsdatascience.com/speed-typing-test-project-with-python-da1a56987a5b

使用 Python 开发速度打字测试项目以评估准确性和打字速度

Bharath KTowards Data Science Bharath K

·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 22 日

--

图片来源 Spencer DavisUnsplash

每个拥有电子设备的个人通常都会在其各自的设备上打字,无论是笔记本电脑、手机还是个人电脑。在现代世界中,通过项目打字是一种更为广泛使用且方便的方法,可以完成各种任务。

就我个人而言,我打字的频率相当高,打字的内容和材料也各不相同。这些内容的范围从在 Medium 上打字的文章、分析和编写数据科学项目,到写重要的邮件,或者只是浏览互联网。虽然写作有助于产生想法和有效思考,但我不能否认我在打字上花费的时间比写作要多。

无论人们是否与我处于同一范围,或者每个人的情况是否不同,保持在打字时的‘A’状态总是一个好主意。在这个项目中,我们将使用 Python 设计一个简单的速度打字测试,以帮助评估你的准确性、错误率和打字速度。

我们将在控制台界面上开发这个项目,并以每秒字数的形式打印错误率和总体得分。对于那些更有好奇心的开发者,我推荐查看使用 Python 开发的高级 GUI 界面,这样可以使项目更具吸引力和美观。我已经在下面提供了相关文章的链接。

## 使用 Python 的高级 GUI 界面

使用更现代的 Python 开发接口来开始你的项目

towardsdatascience.com

开发速度测试项目:

Fiona Murray拍摄,Unsplash

在这部分文章中,我们将开发“速度测试”软件,以报告用户打字的分数和错误率。测试应当进行多次,以获得平均评分,从而准确确定真正的数值。

这个项目有多种不同的方法来处理,我将使用最直接、最简单的方法来实现预期的解决方案。我建议安装下面提供的库来简化这个过程。

pip install wonderwords

你可以通过以下链接访问官方 pip 安装 Python 包索引网站,了解更多关于该库的信息,或者通过以下快速入门文档网站了解更多内容。为了让自然语言处理任务更易于理解,我建议查看下面链接提供的我之前关于正则表达式操作符的文章。

## 用 4 种基本正则表达式操作符简化自然语言处理!

理解四种基本正则操作来清理几乎任何类型的数据。

towardsdatascience.com

导入所需的库:

使用 Python 构建我们的速度测试软件的第一步是导入所有必要的库。我们将使用 wonder words 库导入随机句子类,这将允许我们生成随机句子,供用户进行打字测试。我更喜欢使用这个库作为一种更通用的方法,以便每次需要进行速度测试时随机生成不同的段落。

对于那些希望采用不同方法的开发者,如果不想使用之前提到的库,我们可以使用随机库并自己输入句子和创建列表。随机库中的选择功能对于将各种段落选项随机显示给测试者非常有用。时间库模块对跟踪用户的打字速度至关重要。以下是所有所需库导入的列表。

from wonderwords import RandomSentence
import random
import time

随机句子生成:

一旦导入了所有必要的库,我们可以继续下一步。我们将创建一个句子列表和一个作为字符串变量的句子段落。然后我们将创建一个 for 循环,通过这个循环我们将使用“wonder words”库中的随机句子模块。一旦将以下类分配给所需的变量,我们可以生成随机句子。

每个随机生成的句子都被附加到一个列表中,然后转换成一个字符串段落,其中所有包含相应句子的段落被一起存储。这些随机生成的段落将展示给用户。展示后,用户可以进行打字测试,以测试适当的速度和错误率。

sent_list = []
sent_para = ""

for i in range(5):
    sent = RandomSentence()
    random_sent = sent.sentence()
    sent_list.append(random_sent)
    sent_para += random_sent +  " "

错误率计算:

一旦随机段落存储在一个变量中,我们可以继续定义计算测试者打字错误率的下一步。我使用了一种简单的方法来计算错误率,以便于这个项目的简单性。请注意,这可能不是计算错误率的最有效方法。我将在即将到来的部分中涵盖更多的改进和进一步的发展。

在这种方法中,我们计算句子段落的长度,并在此范围内创建一个循环。然后,我们将比较每次输入的原始句子段落与输入的段落。每次字符不匹配时,错误计数会增加。总错误百分比通过将错误计数除以总长度,并乘以 100 来计算。以下是执行该功能的代码片段。

def error_rate(sent_para, typed_para):
    error_count = 0

    length = len(sent_para)

    for character in range(length):
        try: 
            if sent_para[character] != typed_para[character]:
                error_count += 1

        except:
            error_count += 1

    error_percent = error_count/length * 100
    return error_percent

最终得分和错误百分比:

在这个项目的最后一步,我们将输入一个句子,表示速度测试即将开始,且必须在规定时间内准确打印出适当的段落,以达到最佳分数。开始时间和结束时间会被记录,直到用户输入了相应的段落。总时间通过从结束时间中减去开始时间来计算。错误率通过本文节中之前定义的函数来计算。

我增加了一个创建 if 循环的额外步骤,以测量输入句子的错误百分比。如果错误百分比超过 50,那么计算出的分数可能不准确,可能需要重新测试。如果错误百分比低于 50%,我们可以报告每秒单词数和总单词数。以下是执行速度测试操作的代码块。

print("Type the below paragraph as quickly as possible with as few mistakes to get a high score: \n")
print(sent_para)
print("\n")

start_time = time.time()
typed_para = input()
end_time = time.time()

time_taken = end_time - start_time

error_percent = error_rate(sent_para, typed_para)
print("\n")

if error_percent > 50:
    print(f"Your error rate {error_percent} was quite high and hence your accurate speed could not be computed.")

else:
    speed = len(typed_para)/time_taken
    print("******YOUR SCORE REPORT******")
    print(f"Your speed is {speed} words/sec")
    print(f"The error rate is {error_percent}")

速度测试项目现在已完成。读者可以继续进行自己的测试并检查他们的打字速度!然而,对于那些更好奇和感兴趣的开发者,我们将进行一次测试运行。我还会建议一些额外的改进,以使这个项目在即将到来的部分中变得更加有趣和用户友好。

测试运行和额外改进:

作者截图

一旦你在各自的控制台中运行程序(我使用的是 Visual Studio Code 编辑器,但也可以使用命令提示符),你应该能够相应地测试工作代码。在上面的截图中,读者可以注意到由 wonder words 库生成的随机句子,原始段落下面是我输入的段落。

一旦我输入所需段落并按下回车键,我们可以相应地查看评分报告。从上面的截图中,我们可以注意到我的单词速度约为每秒 4.3 个单词,错误率为 0.59\。为了获得更精确的测试分数,我建议运行上述程序并取至少五次测试的平均值。

我们在本节中将讨论的另一个重要话题是进一步增强此项目功能和风格方面的不同改进。一些建议如下:

  1. 为了更具自定义性地生成句子,使用自己定制的句子和随机选择函数是一个不错的选项。然而,如果读者想更进一步,Open AI 提供了一个用于高级故事集成的绝佳选项。

  2. 错误率的计算可能略有偏差,因为一个字符错误可能导致多个故障。更好的方法可能是考虑原始段落和输入段落中的所有单词,然后比较两个列表。

  3. 最后的建议是将整个项目转移到 GUI 界面上,因为控制台界面可能显得平淡。我推荐查看我之前关于 GUI 的文章,以快速了解一些在 Python 中可用的值得注意的 GUI 选项的入门代码。

## Python 开发者的 7 款最佳 UI 图形工具及入门代码

Python 中开发酷炫用户界面技术的七款最佳 UI 图形工具

towardsdatascience.com

结论:

图片来源于 Paul KansonkhoUnsplash

“打字是说话的未来,不要忘记它,是特性的兄弟。”

Deyth Banger

打字现在是我们生活中的重要部分,是我们不断进行的必要活动。虽然我们打字的次数很多,而且随着时间的推移变得越来越自然,但人们可能会好奇他们打字的速度和准确度。了解这一点的一个好方法是不断提高打字速度,以提高生产力。

在这个项目中,我们开发了一个用 Python 制作的速度测试软件,使我们能够输入建议的特定段落,并在输入错误百分比低于 50 的段落后,获得我们的打字分数和百分比错误。我们还讨论了可以进一步改进和提升这个项目的其他方法。

如果你希望在我的文章发布后第一时间收到通知,请查看以下链接来订阅电子邮件推荐。如果你希望支持其他作者和我,请订阅以下链接。

[## 通过我的推荐链接加入 Medium - Bharath K

阅读 Bharath K 的每个故事(以及 Medium 上成千上万的其他作家的故事)。你的会员费直接支持…

bharath-k1297.medium.com](https://bharath-k1297.medium.com/membership?source=post_page-----da1a56987a5b--------------------------------)

如果你对本文中提到的各个要点有任何疑问,请随时在下方评论区告诉我。我会尽快回复你。查看一些我的其他文章,看看我的观众们还喜欢阅读什么!

## Jupyter Notebooks 的终极替代方案

讨论一个优秀的 Jupyter Notebooks 替代方案,用于解释数据科学项目

towardsdatascience.com ## 阅读 7 篇最佳研究论文,以开始深度学习项目

七篇经得起时间考验的最佳研究论文,将帮助你创建出色的项目

towardsdatascience.com ## 使用 Python 可视化 CPU、内存和 GPU 工具

分析 CPU、内存使用情况和 GPU 组件,以监控你的 PC 和深度学习项目

towardsdatascience.com

感谢大家坚持看到最后。我希望你们都喜欢阅读这篇文章。祝你们度过美好的一天!

使用 R 树加速你的地理空间数据分析

原文:towardsdatascience.com/speed-up-your-geospatial-data-analysis-with-r-trees-4f75abdc6025?source=collection_archive---------0-----------------------#2023-05-21

图片由 Mathias Arlund 提供,来自 Unsplash

学习如何大幅提升空间搜索的性能

Markus HubrichTowards Data Science Markus Hubrich

·

关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 5 月 21 日

--

几年前,我在做一个副项目。我想创建一个推荐本地珍宝的网页应用,例如咖啡馆、书店或隐秘酒吧。这个想法是将所有这些用户可触及的兴趣点显示在地图上。由于数据集中有成千上万的兴趣点,我必须巧妙地过滤出用户指定范围内的数据点。最简单的方法是计算用户与每个兴趣点之间的距离,并丢弃所有超出指定范围的点。特别是对于像我这样的大数据集,这种方法通常会导致较长的处理时间。

当然,必须有更好的方法,因为响应时间在互动应用程序中非常重要。这时我遇到了数据结构R-tree。这些树用于快速的空间访问和搜索。使用 R-tree,我能够快速隔离靠近用户位置的兴趣点,并将其显示在地图上。这大大提升了我的网页应用的响应时间——仅仅四行额外的代码!

在这篇文章中,我解释了 R-trees 是什么以及它们是如何工作的。前两部分通过纽约市的街道树示例进行了说明。第三部分展示了如何在 Python 中使用这种数据结构来加速地理空间数据处理例程。

通过分析纽约市的树木来学习 R-trees

假设我们被要求分析纽约市的社区与其树木健康之间是否存在相关性。NYC 开放数据门户提供了一个街道树普查数据集,包括每棵树的物种、直径、健康状况的感知和地理位置。

[## 2015 街道树普查 - 树木数据 | NYC 开放数据

数据来自 TreesCount! 2015 街道树普查,由志愿者和纽约市公园部组织的工作人员进行…

data.cityofnewyork.us](https://data.cityofnewyork.us/Environment/2015-Street-Tree-Census-Tree-Data/pi5s-9p35?source=post_page-----4f75abdc6025--------------------------------)

首先,我们想统计上东区的街道树数量。下面的伪代码片段遍历数据集trees,并检查一棵树是否落在upper_east_side边界内:

total_count, tree_count = 0, 0
for tree in trees:
    total_count += 1
    if upper_east_side.contains(tree):
        tree_count += 1print_results(num_tests=total_count, num_trees=tree_count)
**>>>** Total number of trees tested: 683,788
**>>>** Number of trees in Upper East Side: 8,807

我们发现上东区大约有 9 千棵树。然而,我们必须测试总共 68 万棵树才能得到这个结果。下方的动画可视化了我们测试那些距离目标社区几英里的树,因此可以很容易地被忽略。但我们如何能排除远离的树木以减少昂贵的计算,从而实现显著的性能提升呢?

我们几乎可以免费获得的一项信息是多边形的边界框(可以通过其节点的最小值和最大值来确定)。此外,测试一个点是否落在矩形内非常简单,只需进行四次比较操作(点必须大于或等于左下角,并且小于或等于右上角)。现在,假设 bounding_box 是一个数据集,包含了上东区周围紧密矩形内的所有树木(在下一节中我们将学习如何轻松获得这样的矩形)。考虑到这一点,可以得出:

total_count, tree_count = 0, 0
for tree in bounding_box:
    total_count += 1
    if upper_east_side.contains(tree):
        tree_count += 1print_results(num_tests=total_count, num_trees=tree_count)
**>>>** Total number of trees tested: 10,768
**>>>** Number of trees in Upper East Side: 8,807

动画的右侧演示了我们现在仅测试潜在候选者。这些是位于多边形附近的树,即落在其边界框内的点。通过忽略远离的树木,我们将测试数量从 684k 减少到 11k —— 降低了 60 倍! 在下一节中,我们将看到 R-trees 正是利用了这一点。

() 纽约市的所有树木都经过测试 | () 仅测试位于上东区边界框内的树木。图像由作者提供,地图数据来自 © Mapbox 和 © OpenStreetMap

用于空间搜索的数据结构:R-tree

R-trees 是基于树的数据结构,用于高效地创建空间索引。R-tree 通常用于快速空间查询或加速最近邻搜索 [1]。一个常见的应用场景是存储兴趣点的空间信息(例如餐馆、加油站、街道等)。借助 R-trees,可以快速检索到某位置一定距离内的所有兴趣点。反过来,这些结果可以在地图上或导航系统中显示。

R-tree 的基本思想很简单:树的叶节点保存空间数据,而分支节点对应于包含所有子节点的最小边界框。通过这种结构,R-tree 将空间划分为矩形,随着树的增长这些矩形变得更为精细。以下示例对此进行了说明。

() R-tree 将曼哈顿划分为多个矩形 | () 对应的树结构。图像由作者提供,地图数据来自 © Mapbox 和 © OpenStreetMap

R 树被查询以获取一个矩形,即我们想要检索包含在此搜索窗口内的所有数据。请记住,每个非叶节点对应一个包含其所有子节点的边界框。为了执行搜索查询,我们只需沿着树的分支移动,并且沿着与给定矩形相交的路径直到到达叶节点。这些叶节点,因此我们的数据点,包含在搜索矩形内并完成查询。下面的动画演示了我们可以通过忽略不符合搜索条件的整个分支大大减少搜索操作的数量。

(左侧) 不与搜索矩形(红色)相交的边界框(黑色)被逐步忽略 | (右侧) 搜索查询通过遵循与搜索矩形相交的路径来完成。图片由作者提供。

Python 中的 R 树

Python 包Rtree提供了 R 树数据结构的实现,并提供了许多方便的功能,如最近邻搜索、交集搜索或多维索引。

[## Rtree: Spatial indexing for Python - Rtree 0.9.4 documentation

Rtree 是 libspatialindex 的 ctypes Python 包装器,为 Python 提供了许多先进的空间索引功能。

rtree.readthedocs.io

我们可以方便地使用 Python 的包管理器pip安装这个包:pip install Rtree

基础知识

在处理像点或多边形这样的几何体之前,我们先了解一下Rtree包的基本用法。

index模块帮助我们构建空间索引。这个索引通过插入我们对象的边界框自动构建起来。边界框通过指定它们的左侧、底部、右侧和顶部坐标来定义。请注意,在执行查询时,我们将边界框与一个标识符(在上面的示例中为01)一起插入。该 ID 将帮助我们在执行查询时识别边界框:

该索引被查询以获取给定矩形内的对象,再次由其左侧、底部、右侧和顶部坐标指定。intersection方法的结果是在搜索窗口内包含的对象的 ID(示例 1-3)。如果搜索窗口超出我们在索引中的数据范围,结果为空(示例 4)。类似地,我们使用nearest方法来找到离给定搜索窗口最近的k个对象:

处理点、线和多边形

在前面的部分中,我们看到索引是通过插入对象的边界框来构建的。现在,我们希望继续使用点、线和多边形来处理这些对象。包Shapely提供了一种在 Python 中处理这些类型几何体的简单方法:

上面,我们首先创建一个点,一个线和一个多边形。接下来,这些对象的边界框被使用 ID 012插入索引中。我们现在查询不同的搜索窗口:

下面的插图显示了几何图形和搜索窗口:

绿色: 点、线和多边形。红色: 搜索窗口。图片由作者提供。

搜索上东区的所有树木

我们终于具备了提取上东区所有树木所需的一切!我们将通过下面的代码片段进行说明,完整版本可以在这里找到。

绿色: 纽约市的树木。蓝色: 上东区。橙色: 上东区的边界框。图片由作者提供,地图数据来自© Mapbox和© OpenStreetMap

首先,我们使用GeoPandas包加载所有所需的几何图形:

接下来,我们创建一个包含纽约市所有树木的 R 树索引:

现在,我们生成一个潜在候选项的列表,即所有在上东区边界框内的树木:

最后,我们遍历所有潜在的候选项,提取完全位于上东区内的那些:

结论

在这篇文章中,我们了解了 R 树如何通过将底层空间划分为矩形来组织地理信息。这种结构使 R 树在空间查找中极其快速。在我们的纽约市街道树木示例中,使用 R 树将操作次数减少了 60 倍。我们还看到了如何在 Python 中使用 R 树。我们的示例中的加速仅通过四行代码实现:初始化索引(1 行),构建索引(2 行),以及使用intersection函数找到附近的候选项(1 行)。

那么,为什么 R 树不是无处不在呢?虽然通过减少搜索操作次数节省了时间,但我们在构建索引时却浪费了时间。后者我们必须实际遍历整个数据集。这使得 R 树不适合只需要少量搜索的应用程序或索引经常变化的应用程序(由于树的重新平衡)。

自 1984 年 Antonin Guttman 发明 R 树以来,它们已经取得了长足的进步。如今,它们被广泛应用于各种领域,例如计算机图形学[2]、视频游戏[3]、交通控制系统[4],以及最显著的空间数据管理数据库[5]。也许在你下一次的地理空间数据分析中也会用到!

参考文献

[1] A. Guttman,R-Trees: A Dynamic Index Structure for Spatial Searching(1984),1984 年 ACM SIGMOD 国际数据管理会议论文集,第 47-57 页

[2] D. Feldmann,使用 R 树加速光线追踪(2015),第十届计算机图形学理论与应用国际会议论文集,第 247–257 页

[3] A. Kinziabulatov,在 Unity 中优化 R 树插入:一个类似 Bomberman 的例子(2023),Medium

[4] Y. Manolopoulos, A. Nanopoulos, A. Papadopoulos 和 Y. Theodoridis,R 树:理论与应用(2006),Springer

[5] S. Bressan, J. Küng 和 R. Wagner,数据库与专家系统应用(2006),Springer

数据集

纽约市公园与娱乐部,2015 年街道树木普查——树木数据(2016),纽约市开放数据

纽约市城市规划部,2010 年社区划分区域(NTAs)(2013),纽约市开放数据

加速你的 Python 技能

原文:towardsdatascience.com/speed-up-your-python-skills-in-2023-e680f4c56f37

七个技巧助你更上一层楼

Janik 和 Patrick TinzTowards Data Science Janik 和 Patrick Tinz

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 16 日

--

图片来源 Emile PerronUnsplash

Python 是数据科学领域使用最广泛的编程语言,其受欢迎程度不断增长。近年来,整个数据科学领域也得到了巨大的发展。

在这篇文章中,我们将向你展示七个提高 Python 技能的技巧。通常是那些小细节能产生大的差异。这些提示将丰富你作为数据科学家的生活。因此,我们提供了七个可以立即实践的技巧。保持好奇!

作为数据科学家,你经常需要处理大量的数据。因此,你必须在运行时间和内存方面高效编码。你的 Python 代码也应该结构良好且易于阅读。这些提示将帮助你编写高效且易读的 Python 代码。

提示 1: 加速 NumPy

NumPy 是一个用于高效处理数组的 Python 库。它还提供了快速和优化的矢量化操作。但!它不支持并行处理。作为 NumPy 的替代品,你可以使用 NumExpr

NumExpr 的性能显著优于 NumPy,因为它支持多线程。此外,它避免了为中间结果分配内存。

首先,你需要安装 NumPy 和 NumExpr 包。例如:

$ pip install numpy numexpr

看看这个例子并尝试一下。

import numpy as np
import numexpr as ne
import timeit

var1 = np.random.random(2**27)
var2 = np.random.random(2**27)

%timeit np.sin(var1) / np.cos(var2)
# 2.73 s

%timeit ne.evaluate("sin(var1) / cos(var2)")
# 566 ms

哇!该语句在 NumExpr 下执行速度大约提高了 5 倍。因此,如果你想加快 NumPy 语句的速度,这为你提供了一种实现方法。

NumExpr 在处理大数组时效果最佳。如果你有一个强大的多核计算机,它也能发挥其最大性能。因此,当这两个条件都满足时,我们推荐使用 NumExpr。对于小规模数组操作,你也可以使用 NumPy,因为性能差异非常小。原因是 NumExpr 将数组操作数拆分成小块,这些小块很容易适应 CPU 缓存。这些小块在 CPU 的可用核心之间分配,从而实现并行执行。

如果你想了解更多关于 NumExpr 的内容,可以查看NumExpr 的 GitHub库。

提示 2:pandas apply()的快速替代方案

pandas 的 apply()函数可以在数据框的一个轴上执行函数。许多程序员将 apply()函数与 lambda 函数结合使用。但你如何提高 apply()函数的性能呢?

你可以使用 swifter 包。这个包将函数非常快速地应用于数据框或序列。pandas 的 apply()函数在一个核心上运行,而 swifter 则提供了多个核心支持。

首先,你需要安装 swifter 包。

$ pip install swifter

安装后,你可以直接尝试它。

import pandas as pd
import numpy as np
import swifter
import timeit

df = pd.DataFrame({'a': np.random.randint(7, 10, 10**7)})

# pandas apply()
%timeit  df.apply(lambda x: x**7)
# 54 ms

# swifter.apply()
%timeit  df.swifter.apply(lambda x: x**7)
# 37.5 ms

这个简单的例子显示了 swifter.apply()函数的运行时间更快。尤其是在多核的强大计算机上,这一差异尤其明显。如果你在下一个项目中需要性能提升,考虑使用 swifter 包。

提示 3:使用内置的 Python 函数

你经常实现一个函数却不知道它在 Python 中已经存在。尤其是如果你来自其他编程语言,如 C 或 C++。首先,你应该总是检查 Python 内置函数是否已经存在。Python 内置函数比自定义实现要快得多,因此你应该总是使用它们。以下示例演示了这一点。

import numpy as np
from time import perf_counter

result_list = []
company_list = ["Tesla", "Block", "Palantir", "Apple"]
company_list_sample = np.repeat(company_list, 10**7)

start = perf_counter()
for company in company_list_sample:
  result_list.append(company.lower())
print(perf_counter()-start)
# 17.13 s

start = perf_counter()
result_list = map(str.lower, company_list_sample)
print(perf_counter()-start)
# 0.97 s

在上面的代码中,我们将一个包含四个条目的列表复制了 1 千万次,因此得到一个包含 4 千万个条目的列表。然后我们将列表中的字符串转换为小写。你可以看到,内置函数的速度约快了 17 倍。特别是在大量数据的情况下,这个提示带来了巨大的性能提升。所以要使用内置函数!

还有许多内置函数,如 min()、max()、all()等。如果你需要特定的 Python 函数,可以自己查找。这很值得!

提示 4:使用列表推导代替循环

程序员经常将列表与循环结合使用来存储计算结果。然而,这种方法在运行时间上效率不高。因此,最好使用列表推导,它具有更好的性能。以下示例显示了性能的差异。

import numpy as np
from time import perf_counter

result_list_loop = []
result_list_com = []

number_round = 10000000

start = perf_counter()
for i in range(number_round):
  result_list_loop.append(i*i)
print(perf_counter()-start)
# 1.47 s

start = perf_counter()
result_list_com = [i*i for i in range(number_round)]
print(perf_counter()-start)
# 0.69 s

print(result_list_com[10])
# 100

从这个示例中我们学到了什么?尽可能使用列表推导式。列表推导式在编程中有些争议。一些程序员觉得语法难以阅读,因为一行代码表达了所有语句。在我们看来,语法清晰简洁。这是一个口味问题,但列表推导式的性能更好。

列表推导式以开括号[开始。然后是 for 循环中的计算。接着是循环头,包含三个元素(关键字 for,运行变量,循环长度)。列表推导式以闭括号]结束。一旦你理解了语法,你可以更紧凑地编写 for 循环。

那么在内存使用方面呢?我们如何减少内存空间?如果我们想对大列表进行进一步操作,这一点尤其建议。在我们的示例中,我们在列表中存储了 10000000 个值。但我们是否必须直接保存所有条目,还是仅在需要时才保存它们?

在这些情况下,我们可以使用生成器。生成器在需要时创建一个列表项。因此,生成器需要更少的内存并且运行时间更好。看看下面的示例。

import sys 
from time import perf_counter

print(sys.getsizeof(result_list_com), 'bytes')
# 89095160 bytes

start = perf_counter()
result_gen = (i*i for i in range(number_round))
print(perf_counter()-start)
# 0.22 ms

print(sys.getsizeof(result_gen), 'bytes')
# 112 bytes

print(list(result_gen)[10])
# 100

我们可以执行与之前示例相同的所有操作。唯一的区别是我们现在使用的是()而不是[]。我们存储的是生成器而不是列表。这种方法在内存上更为高效。检查一下你是否可以在项目中使用列表推导式或生成器。它们可以提高性能并减少内存使用。

提示 5:使用双星号语法**合并字典

如何合并字典?你可以用一行代码来实现。我们使用了星号语法**。在下面的示例中,你可以看到它是如何工作的。

dict_1 = {'company': 'Tesla', 'founding': 2002}
dict_2 = {'company': 'Tesla', 'founding': 2003, 'CEO': 'Elon Musk'}

dict_merged = {**dict_1, **dict_2}
print(dict_merged)
# {'company': 'Tesla', 'Founding': 2003, 'CEO': 'Elon Musk'}

首先,我们定义两个字典,它们有相同和不同的键值对。特斯拉成立于 2003 年,所以 dict_2 更加最新。如果两个字典包含相同的键但值不同,则使用最后一个字典的值。合并后,新字典包含所有三个键值对。语法简洁紧凑,因此合并非常简单。而且最棒的是,你可以合并三个或更多字典。这个技巧可以节省大量时间。

另一种方法是 update 方法。此方法更新第一个字典,并不创建副本。看看下面的示例。

dict_1 = {'company': 'Tesla', 'founding': 2002}
dict_2 = {'company': 'Tesla', 'founding': 2003, 'CEO': 'Elon Musk'}

dict_1.update(dict_2)
print(dict_1)
# {'company': 'Tesla', 'Founding': 2003, 'CEO': 'Elon Musk'}

update 方法的缺点是你只能使用一个字典进行更新。如果你未来想要合并字典,请记住这个提示。

提示 6:不要导入不必要的模块

你可能听到过这个提示很多次,但它可以显著提高代码的性能。没有必要导入整个库。你通常只需要其中的某些函数。此外,由于必须先导入整个库,所以你的代码启动时间较长。这不应该是这样的。此外,你还必须通过点表示法访问单独的函数。这非常低效,你应该避免使用点表示法。以下示例演示了这一点。

import math
from time import perf_counter

start = perf_counter()
variable = math.exp(7)
print(perf_counter()-start)
# 8.47-05 s

在这个示例中,我们使用了 math.exp() 函数和点表示法。这导致了代码性能不佳。此外,我们导入了整个 math 库,尽管我们只需要 exp() 函数。

from math import exp
from time import perf_counter

start = perf_counter()
variable = exp(7)
print(perf_counter()-start)
# 4.51-05 s

在这个示例中,我们在没有点表示法的情况下导入了 exp() 函数。通过使用这个技巧,我们可以将代码的运行时间减半。哇,太棒了!

提示 7:使用即时编译器

Numba 是一个即时 (jit) 编译器,能够很好地与 NumPy 循环、数组和函数配合使用。装饰器用于指示 Numba 用 Numba 编译特定函数。Numba 将装饰过的函数即时编译为机器代码,以便所有或部分代码以本地机器代码的速度运行。

首先,我们需要通过 pip 安装 Numba。

pip install numba

安装成功后,你可以使用 Numba。请查看以下示例:

import numpy as np
from numba import jit
import timeit

var = np.random.random(10**7)
num_loop = 10000

def foo(var):
    result = 0
    for i in range(num_loop):
        result += 1
    result = np.sin(var) + np.cos(var)
    return result               
%timeit foo(var)
# 154 ms

@jit(nopython=True)
def foo_numba(var):
    result = 0
    for i in range(num_loop):
        result += 1
    result = np.sin(var) + np.cos(var)
    return result    

%timeit foo_numba(var)
# 76.3 ms

你可以看到,上述 foo 函数的装饰器加快了代码的执行速度。装饰器 nopython=True 表明编译将不涉及 Python 解释器。Numba 加速了循环和 NumPy 三角函数的执行。然而,它不能与所有 Python 函数一起使用。以下是 Numba 的优缺点:

缺点:

  • Numba 不支持 pandas。

  • 不支持的代码通过解释器执行,并且有额外的 Numba 开销。

  • 仅对 M1/Arm64 提供非官方支持。

优点:

  • 对 NumPy 数组和函数以及循环的支持非常好。

  • 支持 Nvidia CUDA。它可以很好地用于基于 NumPy 的神经网络开发。

缺点和优点表明 Numba 应主要用于 NumPy 操作。此外,你应该总是在开始时检查 Numba 是否适合相应的实现。

结论

在这篇文章中,我们学习了如何提高代码的运行时间和内存效率。学到的教训:

  • NumPy 不支持并行处理。你可以使用 NumExpr 来处理这个问题。

  • Pandas 的 apply() 函数可以通过 swifter 加速。

  • 检查是否有内置函数。

  • 使用列表推导而不是循环。检查生成器是否适合你的项目。

  • 使用双星号语法合并字典**。

  • 不要导入不必要的模块。

  • 如果你遇到运行时间问题,你可以使用即时编译器。即时编译器可以加速你的代码。

👉🏽 您可以在我们的数字产品页面上找到所有免费的资源!

👉🏽 加入我们的每周免费 Magic AI 通讯,获取最新的 AI 更新!

免费订阅 以便在我们发布新故事时收到通知:

[## 每当 Janik 和 Patrick Tinz 发布新内容时,您将收到电子邮件。

每当 Janik 和 Patrick Tinz 发布新内容时,您将收到电子邮件。通过注册,您将创建一个 Medium 账户,如果您还没有的话…

tinztwinspro.medium.com

在我们的关于页面了解更多关于我们的信息。不要忘记在X上关注我们。非常感谢您的阅读。如果您喜欢这篇文章,请随意分享。祝您一天愉快!

使用我们的链接注册 Medium 会员,阅读无限制的 Medium 故事。

在 Mozilla Common Voice 上的口语语言识别——音频变换。

原文:towardsdatascience.com/spoken-language-recognition-on-mozilla-common-voice-audio-transformations-24d5ceaa832b?source=collection_archive---------1-----------------------#2023-08-13

Sergey VilovTowards Data Science Sergey Vilov

·

关注 发表于 Towards Data Science ·5 min read·2023 年 8 月 13 日

--

图片由 Kelly Sikkema 提供,来源于 Unsplash

这是基于 Mozilla Common Voice 数据集的第三篇关于语音语言识别的文章。在 第一部分,我们讨论了数据选择和数据预处理,在 第二部分 中我们分析了几种神经网络分类器的性能。

最终模型达到了 92% 的准确率和 97% 的配对准确率。由于此模型存在较高的方差,通过添加更多数据可能会提高准确率。获取额外数据的一种非常常见的方法是通过对现有数据集执行各种变换来合成数据。

在本文中,我们将考虑 5 种流行的音频数据增强变换:添加噪声、改变速度、改变音调、时间掩蔽和剪切 & 拼接。

教程笔记本可以在 这里 找到

为了说明,我们将使用来自 Mozilla Common Voice(MCV)数据集的样本 common_voice_en_100040。这是句子 The burning fire had been extinguished

import librosa as lr
import IPython

signal, sr = lr.load('./transformed/common_voice_en_100040.wav', res_type='kaiser_fast') #load signal

IPython.display.Audio(signal, rate=sr)

原始样本 common_voice_en_100040 来自 MCV

原始信号波形(作者提供的图像)

添加噪声

添加噪声是最简单的音频增强方法。噪声量由信噪比(SNR)来表征——即最大信号幅度与噪声标准差的比率。我们将生成几个定义为 SNR 的噪声水平,并查看它们如何改变信号。

SNRs = (5,10,100,1000) #Signal-to-noise ratio: max amplitude over noise std

noisy_signal = {}

for snr in SNRs:

    noise_std = max(abs(signal))/snr #get noise std
    noise =  noise_std*np.random.randn(len(signal),) #generate noise with given std

    noisy_signal[snr] = signal+noise

IPython.display.display(IPython.display.Audio(noisy_signal[5], rate=sr))
IPython.display.display(IPython.display.Audio(noisy_signal[1000], rate=sr))

通过将噪声 SNR=5 和 SNR=1000 叠加到原始 MCV 样本 common_voice_en_100040 上获取的信号。

几种噪声水平的信号波形(作者提供的图像)

因此,SNR=1000 听起来几乎像未受干扰的音频,而在 SNR=5 时只能区分信号的最强部分。在实践中,SNR 级别是一个超参数,取决于数据集和选择的分类器。

改变速度

改变速度的最简单方法就是假装信号有不同的采样率。然而,这也会改变音调(声音的频率高低)。增加采样率会使声音听起来更高。为了说明这一点,我们将对我们的示例“增加”采样率 1.5 倍:

IPython.display.Audio(signal, rate=sr*1.5)

使用虚假采样率获取的信号用于原始 MCV 样本 common_voice_en_100040(作者生成)。

改变速度而不影响音高更具挑战性。需要使用相位声码器(PV)算法。简言之,输入信号首先被分割成重叠的帧。然后,通过应用快速傅里叶变换(FFT)计算每帧内的频谱。播放速度通过以不同的速率重新合成帧来修改。由于每帧的频率内容未受影响,因此音高保持不变。PV 在帧之间进行插值,并使用相位信息实现平滑。

对于我们的实验,我们将使用来自这个PV 实现的stretch_wo_loop时间伸缩函数。

stretching_factor = 1.3

signal_stretched = stretch_wo_loop(signal, stretching_factor)
IPython.display.Audio(signal_stretched, rate=sr)

通过改变原始 MCV 样本 common_voice_en_100040 的速度获得的信号(由作者生成)。

速度增加后的信号波形(图片由作者提供)

因为我们增加了速度,所以信号的持续时间减少了。然而,可以听到音高没有变化。请注意,当伸缩因子很大时,帧间的相位插值可能效果不好。因此,变换后的音频可能会出现回声伪影。

改变音高

要在不改变速度的情况下改变音高,我们可以使用相同的 PV 时间伸缩,但假装信号具有不同的采样率,以使信号的总持续时间保持不变:

IPython.display.Audio(signal_stretched, rate=sr/stretching_factor)

通过改变原始 MCV 样本 common_voice_en_100040 的音高获得的信号(由作者生成)。

为什么我们还要使用这个 PV,而librosa已经有time_stretchpitch_shift函数?这些函数会将信号变换回时间域。当你需要后续计算嵌入时,你将浪费时间在冗余的傅里叶变换上。另一方面,很容易修改stretch_wo_loop函数,使其产生傅里叶输出而不进行逆变换。也可以尝试深入librosa代码以获得类似的结果。

时间掩蔽和切割&拼接

这两种变换最初在频率域中提出(Park 等,2019)。其想法是通过使用预计算的频谱进行音频增强以节省 FFT 的时间。为了简单起见,我们将演示这些变换如何在时间域中工作。所列操作可以通过用帧索引替换时间轴轻松转移到频率域。

时间掩蔽

时间掩蔽的想法是遮盖信号中的随机区域。神经网络将更少地学习到无法泛化的信号特定时间变化。

max_mask_length = 0.3 #maximum mask duration, proportion of signal length

L = len(signal)

mask_length = int(L*np.random.rand()*max_mask_length) #randomly choose mask length
mask_start = int((L-mask_length)*np.random.rand()) #randomly choose mask position

masked_signal = signal.copy()
masked_signal[mask_start:mask_start+mask_length] = 0

IPython.display.Audio(masked_signal, rate=sr)

通过对原始 MCV 样本 common_voice_en_100040 应用时间掩蔽变换获得的信号(由作者生成)。

时间掩蔽后的信号波形(掩蔽区域用橙色标示)(图片由作者提供)

Cut & splice

这个想法是用另一个具有相同标签的信号的随机片段替换信号的随机选定区域。实现几乎与时间掩蔽相同,只是用另一个信号的片段代替了掩蔽。

other_signal, sr = lr.load('./common_voice_en_100038.wav', res_type='kaiser_fast') #load second signal

max_fragment_length = 0.3 #maximum fragment duration, proportion of signal length

L = min(len(signal), len(other_signal))

mask_length = int(L*np.random.rand()*max_fragment_length) #randomly choose mask length
mask_start = int((L-mask_length)*np.random.rand()) #randomly choose mask position

synth_signal = signal.copy()
synth_signal[mask_start:mask_start+mask_length] = other_signal[mask_start:mask_start+mask_length]

IPython.display.Audio(synth_signal, rate=sr)

通过对原始 MCV 样本 common_voice_en_100040(由作者生成)应用 cut&splice 变换得到的合成信号。

cut&splice 变换后的信号波形(从其他信号中插入的片段用橙色标示)(图片由作者提供)

下表显示了 AttNN 模型在验证集上对每个变换的准确率及其典型参数值:

Mozilla Common Voice 数据集上每个变换的 AttNN 准确率及其典型参数(图片由作者提供)。

如所见,这些变换没有显著改变我们基于 MCV 的语音识别系统的准确性。然而,这些变换有可能在某些其他数据集上提升性能。最后,在寻找最佳超参数时,逐个尝试这些变换而不是随机/网格搜索是有意义的。之后,可以将有效的变换结合在一起。

Mozilla Common Voice 上的口语语言识别——第一部分。

原文:towardsdatascience.com/spoken-language-recognition-on-mozilla-common-voice-part-i-3f5400bbbcd8?source=collection_archive---------9-----------------------#2023-08-02

Sergey VilovTowards Data Science Sergey Vilov

·

关注 发表在 Towards Data Science ·6 min read·2023 年 8 月 2 日

--

图片由 Sebastian UnrauUnsplash 提供

其中一个最具挑战性的人工智能任务是识别说话者的语言,以便进行后续的语音转文字转换。例如,当同一家人中讲不同语言的人使用同一个语音控制设备,如车库锁或智能家居系统时,这个问题可能会出现。

在这一系列文章中,我们将尝试通过使用Mozilla Common Voice(MCV)数据集来最大化口语语言识别的准确性。特别是,我们将比较几种神经网络模型,这些模型被训练用来区分德语、英语、西班牙语、法语和俄语。

在第一部分中,我们将讨论数据选择、预处理和嵌入。

数据选择

MCV 迄今为止是最大的公开语音数据集,包括多达 112 种语言的短录音(平均时长 = 5.3 秒)。

对于我们的语言识别任务,我们选择了 5 种语言:德语、英语、西班牙语、法语和俄语。对于德语、英语、西班牙语和法语,我们仅考虑 MCV 中标注的口音,即Deutschland DeutschUnited States EnglishEspañaFrançais de France。对于每种语言,我们从验证过的样本中选择一部分成人记录。

我们使用了 40K/5K/5K 的训练/验证/测试划分。为了获得客观评价,我们确保三组之间的说话者(client_id)不重叠。在数据拆分时,我们首先将测试和验证集填充来自表现不佳的说话者的记录,然后将剩余的数据分配到训练集中。这提高了验证/测试集中说话者的多样性,并导致了对泛化误差的更客观估计。为了避免单一说话者在训练集中占主导地位,我们将每个client_id的记录最大数量限制为 2000 个。平均来说,我们得到了每个说话者 26 个记录。我们还确保女性记录的数量与男性记录的数量匹配。最后,如果最终记录数量低于 40K,我们对训练集进行了上采样。最终的记录分布如下面的图所示。

训练集中的类别分布(图像由作者提供)。

带有指示拆分的最终数据框架可以在这里找到。

数据预处理

所有 MCV 音频文件都以.mp3 格式提供。虽然.mp3 非常适合音乐的紧凑存储,但它在音频处理库(如 python 中的 librosa)中支持有限。因此,我们首先需要将所有文件转换为.wav 格式。此外,原始 MCV 采样率为 44kHz。这意味着最大编码频率为 22kHz(根据奈奎斯特定理)。对于口语语言识别任务来说,这样的频率有些过高:例如,在英语中,大多数音素在会话语音中不会超过 3kHz。因此,我们也可以将采样率降低到 16kHz。这不仅会减少文件大小,还会加快嵌入生成的速度。

这两个操作可以通过ffmpeg的一条命令来执行:

ffmpeg -y -nostdin -hide_banner -loglevel error -i $input_file.mp3 -ar 16000 $output_file.wav

特征工程

相关信息通常通过计算嵌入从音频片段中提取。我们将考虑四种或多或少常见的用于语音识别/口语语言识别任务的嵌入:梅尔频谱图、MFCC、RASTA-PLP 和 GFCC。

梅尔频谱图

梅尔频谱图的原理已广泛讨论 在 Medium上。关于梅尔频谱图和 MFCC 的精彩逐步教程也可以在此处找到。

为了获得梅尔频谱图,首先对输入信号进行预加重滤波。然后,对滑动窗口应用于获得的波形进行连续的傅里叶变换。之后,频率尺度被转换为梅尔尺度,这与人类对间隔的感知是线性的。最后,应用一组重叠的三角滤波器的滤波器组到梅尔尺度上的功率谱,以模拟人耳对声音的感知。

MFCC

梅尔系数高度相关,这可能对一些机器学习算法不利(例如,高斯混合模型更方便使用对角协方差矩阵)。 去相关梅尔滤波器组,梅尔频率倒谱系数(MFCC)通过对对数滤波器组能量进行离散余弦变换(DCT)获得。通常只使用前几个 MFCC。确切步骤详见此处

RASTA-PLP

知觉线性预测(PLP)(Hermansky 和 Hynek,1990)是计算音乐片段嵌入的另一种方法。

PLP 和 MFCC 之间的差异在于滤波器组、等响预加重、强度到响度的转换以及线性预测的应用(Hönig 等,2005)。

PLP 和 MFCC 技术概述(来自 Hönig 等,2005)

有报告称,PLP(Woodland 等,1996)在训练和测试数据之间存在声学不匹配时,比 MFCC 更具鲁棒性。

与 PLP 相比,RASTA-PLP(Hermansky 等,1991)在对数频谱域中执行额外的滤波,这使得该方法对通信通道引入的线性频谱失真更为鲁棒。

GFCC

有报告称,伽玛音调频谱系数(GFCC)比 MFCC 对噪声的敏感性更低(Zhao,2012;Shao,2007)。与 MFCC 相比,伽玛音调滤波器是在等效矩形带宽尺度上计算的(而不是梅尔尺度),并且在计算 DCT 之前应用了立方根操作(而不是对数)。

下图显示了一个示例信号及其不同的嵌入:

示例音频文件及其嵌入(图片由作者提供)。

比较嵌入

为了选择最有效的嵌入,我们训练了 De Andrade 等人(2018 年)提出的注意力 LSTM 网络。由于时间原因,我们只训练了 5000 个剪辑。

下图比较了所有嵌入的验证准确性。

不同嵌入在 5000 个数据集上的表现(图片由作者提供)。

因此,前 13 个滤波器组的 mel 频谱图的表现接近于model_order=13 的 RASTA-PLP。

值得注意的是,mel 频谱图的表现优于 MFCC。这符合之前的说法(见这里,以及这里),即 mel 频谱图是神经网络分类器的更好选择。

另一个观察是,通常系数数量越多,性能会下降。这可能是由于过拟合,因为高阶系数通常代表与说话者相关的特征,这些特征在测试集中(不同说话者被选择)不可泛化。

由于时间限制,我们没有测试任何嵌入组合,尽管之前有观察到它们可能提供更好的准确性。

由于 mel 频谱图的计算速度远快于 RASTA-PLP,我们将在进一步的实验中使用这些嵌入。

在第二部分,我们将运行几个神经网络模型,并选择分类效果最佳的模型。

参考文献

  • De Andrade, Douglas Coimbra, 等. “用于语音命令识别的神经注意力模型。” arXiv 预印本 arXiv:1808.08929(2018 年)。

  • Hermansky, Hynek. “语音的感知线性预测(PLP)分析。” 美国声学学会期刊 87.4(1990 年):1738–1752。

  • Hönig, Florian, 等. “修订感知线性预测(PLP)。” 第九届欧洲语音通信与技术会议。2005 年。

  • Hermansky, Hynek, 等. “RASTA-PLP 语音分析。” IEEE 国际声学、语音与信号处理会议论文集。第 1 卷。1991 年。

  • Shao, Yang, Soundararajan Srinivasan, 和 DeLiang Wang. “在鲁棒说话人识别中引入听觉特征不确定性。” 2007 年 IEEE 国际声学、语音与信号处理会议-ICASSP’07。第 4 卷。IEEE,2007 年。

  • Woodland, Philip C., Mark John Francis Gales, 和 David Pye. “在大词汇量语音识别中提高环境鲁棒性。” 1996 IEEE 国际声学、语音与信号处理会议论文集。第 1 卷. IEEE, 1996.

  • Zhao, Xiaojia, Yang Shao, 和 DeLiang Wang. “基于 CASA 的鲁棒说话人识别。” IEEE 音频、语音与语言处理汇刊 20.5 (2012): 1608–1616.

在 Mozilla Common Voice 上的语音语言识别 — 第二部分:模型。

原文:towardsdatascience.com/spoken-language-recognition-on-mozilla-common-voice-part-ii-models-b32780ea1ee4?source=collection_archive---------6-----------------------#2023-08-06

Sergey VilovTowards Data Science Sergey Vilov

·

关注 发布于 Towards Data Science · 7 分钟阅读 · 2023 年 8 月 6 日

--

图片由 Jonathan Velasquez 提供,来源于 Unsplash

这是基于 Mozilla Common Voice 数据集的语音语言识别系列文章的第二篇。在 第一部分 中,我们讨论了数据选择并选择了最佳嵌入。现在让我们训练几个模型并选择最佳模型。

模型比较

我们现在将对以下模型在完整数据(40K 样本,请参见第一部分获取更多数据选择和预处理信息)上进行训练和评估:

· 卷积神经网络(CNN)模型。我们简单地将语言分类问题视为 2 维图像的分类。基于 CNN 的分类器在语言识别 TopCoder 比赛中显示了有希望的结果。

CNN 架构(图像由作者提供,使用PlotNeuralNet创建)

· 来自 Bartz 等人 2017 的 CRNN 模型。CRNN 结合了 CNN 的描述能力和 RNN 捕捉时间特征的能力。

CRNN 架构(图像来自 Bartz 等,2017)

· 来自 Alashban 等人 2022 的 CRNN 模型。这只是 CRNN 架构的另一个变体。

· AttNN:来自 De Andrade 等人 2018 的模型。该模型最初用于语音识别,后来在智能博物馆项目中应用于口语语言识别。除了卷积和 LSTM 单元,该模型还有一个后续的注意力块,经过训练以根据其分类相关性对输入序列的部分(即计算傅里叶变换的帧)进行加权。

· CRNN* 模型:与 AttNN 相同的架构,但没有注意力块。

· 时间延迟神经网络(TDNN)模型。我们在这里测试的模型用于生成 Snyder 等人 2018 的口语语言识别的 X-vector 嵌入。在我们的研究中,我们绕过 X-vector 生成,直接训练网络来分类语言。

所有模型均基于相同的训练/验证/测试拆分和相同的梅尔谱图嵌入(前 13 个梅尔滤波器组系数)进行训练。模型可以在这里找到。

验证集上的学习曲线如下图所示(每个“epoch”指的是数据集的 1/8)。

各种模型在 Mozilla Common Voice 数据集上的表现(图像由作者提供)。

下表显示了基于 10 次运行的准确率的均值和标准差。

每个模型的准确性(图像由作者提供)

可以清楚地看到,AttNN、TDNN 和我们的 CRNN* 模型表现相似,其中 AttNN 以 92.4% 的准确率排名第一。另一方面,CRNN(Bartz 等人 2017)、CNN 和 CRNN(Alashban 等人 2022)表现相当逊色,CRNN(Alashban 等人 2022)以仅 58.5% 的准确率位列末尾。

然后我们在训练和验证集上训练了获胜的 AttNN 模型,并在测试集上进行了评估。92.4%的测试准确率(男性 92.4%,女性 92.3%)与验证准确率接近,这表明模型没有在验证集上过拟合。

为了理解评估模型之间的性能差异,我们首先注意到,TDNN 和 AttNN 是专门为语音识别任务设计的,并已针对先前的基准进行了测试。这可能是这些模型表现优异的原因。

AttNN 模型与我们的 CRNN 模型(相同架构但没有注意力块)之间的性能差距证明了注意力机制在口语语言识别中的相关性。接下来的 CRNN 模型(Bartz et al. 2017)尽管架构类似,但表现较差。这可能只是因为默认的模型超参数不适合 MCV 数据集。

CNN 模型不具有特定的记忆机制,紧随其后。严格来说,CNN 有某种记忆的概念,因为计算卷积涉及固定数量的连续帧。因此,由于 CNN 的层次结构,较高层会封装更长时间间隔的信息。实际上,得分第二的 TDNN 模型,可以视为 1-D CNN。因此,如果在 CNN 架构搜索上投入更多时间,CNN 模型的表现可能会接近 TDNN。

Alashban 等人 2022 年的 CRNN 模型意外地显示出最差的准确率。有趣的是,该模型最初设计用于在 MCV 中识别语言,并显示出约 97%的准确率,如原始研究所报告。由于原始代码未公开,因此很难确定这种巨大差异的来源。

成对准确率

在许多情况下,用户通常使用不超过 2 种语言。在这种情况下,更合适的模型性能指标是成对准确率,它仅仅是计算在给定语言对上的准确率,忽略所有其他语言的得分。

测试集中 AttNN 模型的成对准确率如下面的表格所示,混淆矩阵旁边,个别语言的召回率在对角线上。平均成对准确率为 97%。成对准确率总是高于准确率,因为只需要区分 2 种语言。

混淆矩阵(左)和 AttNN 模型的成对准确率(右)(图像由作者提供)。

因此,该模型在德语(de)和西班牙语(es)以及法语(fr)和英语(en)之间的区分能力最佳(98%)。这并不令人惊讶,因为这些语言的语音系统差异很大。

尽管我们使用了 softmax 损失来训练模型,但之前有报道指出,使用tuplemax 损失(Wan et al. 2019)在成对分类中可能获得更高的准确率。

为了研究 tuplemax 损失的影响,我们在 PyTorch 中实现了 tuplemax 损失,并重新训练了我们的模型(详见这里)。下图比较了在验证集上评估时 softmax 损失和 tuplemax 损失对准确率和成对准确率的影响。

使用 softmax 和 tuplemax 损失计算的 AttNN 模型的准确率和成对准确率(作者提供的图片)。

可以观察到,当比较整体准确率(成对 t 检验 p 值=0.002)或成对准确率时,tuplemax 损失的表现更差(成对 t 检验 p 值=0.2)。

实际上,即使原始研究也未能清楚地解释为何 tuplemax 损失应该表现更好。以下是作者提出的例子:

tuplemax 损失的解释(来自于 Wan 等人,2019 年的图片)

损失的绝对值实际上并不意味着太多。通过足够的训练迭代,这个例子可能会用一个或另一个损失函数正确分类。

无论如何,tuplemax 损失并非一种通用解决方案,损失函数的选择应该针对每个特定的问题进行谨慎利用。

结论

我们在 Mozilla Common Voice(MCV)数据集的短音频剪辑中实现了 92%的准确率和 97%的成对准确率,涉及德语、英语、西班牙语、法语和俄语。

在初步研究中,比较了 mel 频谱图、MFCC、RASTA-PLP 和 GFCC 嵌入,我们发现带有前 13 个滤波器组分的 mel 频谱图具有最高的识别准确率。

接下来,我们比较了 5 个神经网络模型的泛化性能:CNN、CRNN(Bartz 等人,2017)、CRNN(Alashban 等人,2022)、AttNN(De Andrade 等人,2018)、CRNN*和 TDNN(Snyder 等人,2018)。在所有模型中,AttNN 展示了最佳性能,突显了 LSTM 和注意力模块在语音语言识别中的重要性。

最后,我们计算了成对准确率并研究了 tuplemax 损失的影响。结果表明,与 softmax 相比,tuplemax 损失同时降低了准确率和成对准确率。

总之,我们的结果为 Mozilla Common Voice 数据集上的语音语言识别建立了新的基准。通过结合不同的嵌入和广泛探索有前景的神经网络架构,例如变压器,未来研究可以取得更好的结果。

在第三部分中,我们将讨论哪些音频转换可能有助于提高模型性能。

参考文献

  • Alashban, Adal A., 等人。"使用卷积递归神经网络进行语音识别系统。" 应用科学 12.18 (2022): 9181。

  • Bartz, Christian 等人。“使用深度卷积递归神经网络进行语言识别。” Neural Information Processing: 24th International Conference, ICONIP 2017, Guangzhou, China, November 14–18, 2017, Proceedings, Part VI 24。Springer International Publishing, 2017 年。

  • De Andrade, Douglas Coimbra 等人。“用于语音命令识别的神经注意力模型。” arXiv 预印本 arXiv:1808.08929(2018 年)。

  • Snyder, David 等人。“使用 x-vectors 进行口语语言识别。” Odyssey。第 2018 卷。2018 年。

  • Wan, Li 等人。“用于语言识别的 Tuplemax 损失。” ICASSP 2019–2019 IEEE 国际声学、语音与信号处理会议(ICASSP)。IEEE, 2019 年。

30 个 SQL 查询通过它们的 Pandas 等效体进行解释

原文:towardsdatascience.com/sql-for-people-who-love-pandas-a-10-minute-yet-thorough-tutorial-c189de9d417d

SQL 对喜欢 Pandas 的人来说变得更简单了

Bex T.Towards Data Science Bex T.

·发布于Towards Data Science ·阅读时间 10 分钟·2023 年 6 月 9 日

--

图片由我使用 Midjourney 制作

动机

自 1974 年起 SQL 主导了数据世界,而 Pandas 在 2008 年出现,提供了内置可视化和灵活的数据处理等吸引人的功能。它迅速成为数据探索的首选工具,掩盖了 SQL 的光芒。

但不要被迷惑,SQL 仍然保持其地位。它是数据科学领域第二受欢迎且第三增长最快的语言(见这里)。所以,虽然 Pandas 抢占了风头,SQL 仍然是任何数据科学家必备的技能。

让我们看看当你已经了解 Pandas 时学习 SQL 有多么简单。

连接到数据库

设置 SQL 工作区并连接到示例数据库可能非常麻烦。首先,你需要安装你喜欢的 SQL 类型(PostgreSQL、MySQL 等)并下载一个 SQL IDE。在这里进行这些操作会偏离文章的目的,因此我们将使用一个快捷方式。

具体来说,我们将直接在 Jupyter Notebook 中运行 SQL 查询,而无需额外的步骤。我们需要做的只是使用 pip 安装ipython-sql包:

pip install ipython-sql

安装完成后,启动一个新的 Jupyter 会话,并在笔记本中运行以下命令:

%load_ext sql

一切准备就绪!

为了说明基本的 SQL 语句如何工作,我们将使用Chinook SQLite 数据库,它包含 11 个表。

要将数据集及其 11 个表作为单独的变量加载到我们的环境中,我们可以运行:

%sql sqlite:///data/chinook.db

这个语句以%sql内联魔法命令开头,这告诉笔记本解释器我们将执行 SQL 命令。接下来是下载的 Chinook 数据库所在的路径。

有效的路径应该始终以 sqlite:/// 前缀开头,用于 SQLite 数据库。上面,我们连接到当前目录的 'data' 文件夹中存储的数据库。如果你想传递绝对路径,前缀应该以四个斜杠开头 - sqlite:////

如果你希望连接到不同的数据库风格,你可以参考这篇优秀的文章。

初步查看表格

我们在 Pandas 中总是首先使用 .head() 函数来初步查看数据。让我们学习如何在 SQL 中做到这一点:

数据集也允许用于商业用途。

上述查询中的第一个关键字是 SELECT。它相当于 Pandas 中的括号运算符,用于选择特定列。但 SELECT 关键字后面跟着一个 (星号)。**** 是一个 SQL 操作符,用于从 FROM 关键字之后指定的表中选择所有内容(所有行和列)。LIMIT 用于最小化返回的输出。因此,上述查询等同于 df.head() 函数。

如果你不想选择所有列,你可以在 SELECT 关键字后指定一个或多个列名:

等效的 Pandas 操作是:

tracks[['Name', 'Composer', 'UnitPrice']].head(10)

SQL 中另一个有用的关键字是 DISTINCT。在任何列名之前添加此关键字将返回其唯一值:

SQL 中的注释是用双短横线写的。

计算行数

就像 Pandas 的 DataFrames 上有 .shape 属性一样,SQL 有 COUNT 函数来显示表中的行数:

%%sql

SELECT COUNT(*) FROM tracks

更有用的信息是计算特定列中唯一值的数量。我们可以通过将 DISTINCT 关键字添加到 COUNT 中来做到这一点:

使用 WHERE 子句过滤结果

仅仅查看和计算行数有点无聊。让我们看看如何基于条件过滤行。

首先,让我们查看价格超过一美元的歌曲:

价格超过一美元的歌曲。

条件语句写在 WHERE 子句中,WHERE 子句总是位于 FROM 和 LIMIT 关键字之间。使用条件与在 Pandas 中类似,但我敢说 SQL 版本更易读。

你也可以在使用条件时使用 COUNT 函数。例如,让我们查看价格在 1 到 10 美元之间的歌曲数量:

正如你所看到的,SQL 版本要易读得多。

价格在 1 到 10 美元之间的歌曲数量。

上面我们使用布尔运算符 AND 链接了两个条件。其他布尔运算符(OR、NOT)也类似使用。

现在,让我们查看所有开票城市为巴黎 柏林的发票:

从柏林或巴黎开具的发票

SQL 中的等于运算符只需要一个‘=’(等号)。不等于运算符用‘!=’或‘<>’表示:

账单城市不是柏林或巴黎的发票

使用 BETWEEN 和 IN 进行更简单的过滤

类似的条件非常常用,用简单的布尔运算符编写起来会很麻烦。例如,Pandas 有 .isin() 函数检查一个值是否属于值列表。

如果我们想选择五个城市的发票,我们将不得不编写五个链式条件。幸运的是,SQL 支持类似 .isin() 的 IN 运算符,所以我们不需要:

IN 之后的值列表应该给出为元组,而不是列表。你也可以用 NOT 关键字来否定条件:

对数字列的另一个常见过滤操作是选择范围内的值。为此,可以使用 BETWEEN 关键字,这等同于 pd.Series.between()

选择账单金额在 5 到 15 之间的发票。

检查空值

每个数据源都有缺失值,数据库也不例外。就像有几种方法可以在 Pandas 中探索缺失值一样,SQL 中有特定的关键字用于检查空值的存在。下面的查询计算 BillingState 中缺失值的行数:

你可以在 IS 和 NULL 之间添加 NOT 关键字,以丢弃某一列的缺失值:

使用 LIKE 进行更好的字符串匹配

在 WHERE 子句中,我们根据精确的文本值过滤列。但通常,我们可能希望根据模式过滤文本列。在 Pandas 和纯 Python 中,我们会使用正则表达式进行模式匹配,这非常强大,但正则表达式需要时间来掌握。

作为替代,SQL 提供了‘%’通配符作为占位符,可以匹配任意字符 0 次或多次。例如,‘gr%’ 字符串匹配‘great’,‘groom’,‘greed’,‘%ex%’ 匹配任何中间有‘ex’的文本等。让我们看看如何在 SQL 中使用它:

上述查询找到所有以‘B’开头的歌曲。包含通配符的字符串应出现在 LIKE 关键字之后。

现在,让我们找到所有标题中包含‘beautiful’一词的歌曲:

你还可以在 LIKE 旁边使用其他布尔运算符:

SQL 中还有许多其他通配符具有不同的功能。你可以在这里查看完整列表及其用法。

SQL 中的聚合函数

对列进行基本的算术运算也是可能的。这些运算在 SQL 中称为聚合函数,最常见的有AVG、SUM、MIN、MAX。它们的功能从名称中应当可以清楚地了解到:

聚合函数只会对你使用它们的列给出一个结果。这意味着你不能对一个列进行聚合并选择其他未聚合的列:

你还可以使用WHERE子句将聚合函数与条件结合使用:

也可以对列和简单数字使用算术运算符,如 +、-、*、/。当作用于列时,操作是逐元素进行的:

关于算术运算,有一点需要注意:如果你仅对整数执行操作,SQL 会认为你期望整数作为答案:

%%sql
SELECT 10 / 3

结果是 3 而不是 3.33…。为了获得浮点结果,你应在查询中使用至少一个浮点数或使用全部浮点数以确保安全:

%%sql
SELECT 10.0 / 3.0

利用这些知识,让我们计算歌曲的平均时长(分钟):

如果你注意到上面的列,它的名字写作“生成该列的查询。”由于这种行为,使用长计算,如计算列的标准差或方差,可能会成为问题,因为列名将和查询本身一样长。

为了避免这种情况,SQL 允许使用别名,类似于 Python 中的导入语句别名。例如:

SELECT语句中的单个项后使用as关键字告诉 SQL 我们正在使用别名。这里是更多的示例:

对于长名称的列,你也可以同样轻松地使用别名。

SQL 中的结果排序

就像 Pandas 有sort_values方法一样,SQL 通过ORDER BY子句支持对列进行排序。在子句后传递列名会将结果按升序排列:

我们按作曲家的名字升序排列tracks表。请注意,ORDER BY语句应始终在WHERE子句之后。也可以将两个或多个列传递给ORDER BY

你还可以通过在每个列名后传递DESC关键字来反转排序:

上述查询在按UnitPriceCompose降序排列以及name升序排列后返回三列(ASC是默认关键字)。

SQL 中的分组

Pandas 最强大的功能之一是groupby。你可以用它将表格转变成几乎任何你想要的形状。在 SQL 中,与之非常接近的是GROUP BY子句,也可以用来实现相同的功能。例如,下面的查询计算了每个流派的歌曲数量:

SQL 中的 GROUP BY 和 Pandas 中的groupby之间的区别在于 SQL 不允许选择在 GROUP BY 子句中没有给出的列。例如,在上面的查询中添加一个额外的自由列会产生错误:

然而,你可以在 SELECT 语句中选择任意多的列,只要你在这些列上使用了某种聚合函数:

上面的查询包括了我们迄今为止学习的几乎所有主题。我们按专辑 ID 和流派 ID 进行分组,并为每个组计算了歌曲的平均时长和价格。我们也有效地利用了别名。

我们可以通过按平均时长和流派数量排序来使查询更强大:

注意我们在 ORDER BY 子句中如何使用聚合函数的别名。一旦你对列或聚合函数的结果进行了别名,你可以在查询的其余部分只通过别名来引用它们。

使用 HAVING 条件

默认情况下,SQL 不允许在 WHERE 子句中使用聚合函数进行条件过滤。例如,我们想选择歌曲数量大于 100 的流派。让我们用 WHERE 子句尝试一下:

基于聚合函数结果过滤行的正确方法是使用 HAVING 子句:

HAVING 子句通常与 GROUP BY 一起使用。每当你想使用聚合函数过滤行时,HAVING 子句是最合适的选择!

所有图片均由我自己生成。

摘要

到现在为止,你应该已经意识到 SQL 有多强大,以及与 Pandas 相比,它变得多么可读。尽管我们学到了很多,但我们仅仅是触及了表面。

对于练习题,我推荐Data Lemur或者LeetCode,如果你有冒险的心情。

喜欢这篇文章及其奇特的写作风格?想象一下能访问到更多类似的文章,全部由一个才华横溢、迷人、机智的作者(顺便说一下就是我 😃)撰写。

只需支付 4.99$的会员费用,你将不仅可以访问我的故事,还能获得来自 Medium 上最杰出的头脑的知识宝库。如果你使用我的推荐链接,你将获得我的超级感激和一个虚拟的击掌以支持我的工作。

[## 使用我的推荐链接加入 Medium — Bex T.

获取对我所有⚡优质⚡内容的独家访问权限,以及在 Medium 上无限制地访问所有内容。通过为我购买一份来支持我的工作…

ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----c189de9d417d--------------------------------)

SQL 在 Pandas 上——我新的最爱,速度提升 10 倍。

原文:towardsdatascience.com/sql-on-pandas-usign-duckdb-f7cd238a0a5a

将两者的最佳特点结合起来

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 3 月 23 日

--

照片由 Akira Hojo 提供,发布在 Unsplash

我热衷于寻找加速分析任务的方法。

我以前写过几篇关于内存使用、运行时间和加速数据密集型任务的技巧的文章。由于我们主要每天使用 Pandas,我不断研究改进性能的方法。

## 如何在 Pandas 数据框中进行模糊字符串匹配

匹配文本中没有完美匹配的部分

towardsdatascience.com

这个令人印象深刻的 Python 库功能丰富且可靠。但速度并不是它的超级能力之一。通过一些优化,我们可以获得一些速度提升。但这些技术也有它们的问题。

## 3 种 SQL 优化技术可以瞬间提升查询速度

在完全更换数据模型之前尝试的一些简单技巧

towardsdatascience.com

像 Polars 这样的新替代品也在获得关注。但我仍然偏爱 Pandas,就像许多其他数据专业人士一样。

这就是为什么我们使用数据库而不是内存中的数据框的原因之一。数据库被设计用来从磁盘检索数据并高效地执行计算。

然而,大多数数据科学项目不需要数据库。这就像用大锤子砸坚果一样。这时,像 SQLite 这样的内存数据库就派上用场了。

在内存中,数据库没有单独的进程运行,也不需要复杂的设置。最新版本的 Python 自带 SQLite。

但是 SQLite 也有其缺点。虽然你可以使用 SQL 查询数据库,但速度上的好处并不显著。

## 3 种部署机器学习模型的方法

部署机器学习模型并使其对用户或项目的其他组件可用。

towardsdatascience.com

然而,我们有一个现代化的解决方案——DuckDB。像 SQLite 一样,设置非常简单,你可以将数据库和代码一起移植。而且你可以比 Pandas API 和 SQLite 更快。

使用 DuckDB 查询 Pandas dataframe。

DuckDB 是一个独立的数据库。然而,你也可以将任何 dataframe 转换为 DuckDB 表并进行查询。但在做所有这些之前,这里是如何安装它的:

pip install duckdb

这可能会让你感到惊讶。但这就是我们安装 DuckDB 的方式。尽管它是一个数据库,但它是一个 Python 包进行安装。

由于这是一个简单的内存数据库,它没有像 Postgres 和其他常规数据库那样复杂的配置。你可以设置一些参数,你可以在文档中找到这些参数。

假设我们有一个出租车行程时长的数据集。我们需要计算那些起点在经度 -73.95 西侧的行程的平均时长。

如果你跟随这个教程,你可以使用下面的代码生成测试数据集。

import pandas as pd
import numpy as np

# Define the number of rows in the dataset
num_rows = 10000000

# Generate a random longitude for each row
pickup_longitude = np.random.uniform(low=-38.0, high=-94.0, size=num_rows)

# Generate a random trip duration for each row
trip_duration = np.random.normal(loc=10, scale=5, size=num_rows)

# Create a DataFrame with the pickup longitude and trip duration columns
df = pd.DataFrame(
    {"pickup_longitude": pickup_longitude, "trip_duration": trip_duration}
)

我们使用了计时装饰器来测量运行所需的时间。

## 我在几乎所有数据科学项目中使用的 5 个 Python 装饰器

装饰器提供了一种新的便捷方式,用于从缓存到发送通知的各种操作。

towardsdatascience.com

...

@timing_decorator
def find_avg_trip_duration_in_the_west():
    return df[df['pickup_longitude'] < -73.95]['trip_duration'].mean()

find_avg_trip_duration_in_the_west()

>> Function find_avg_trip_duration_in_the_west took 0.49195261001586914 seconds to run.
>> 9.995189356480168

上面的代码在我的计算机上运行了 0.2 秒。这里是使用 DuckDB API 而不是 Pandas API 的相同查询:

...
import duckdb

@timing_decorator
def find_avg_trip_duration_in_the_west():
    return duckdb.execute(
        'SELECT AVG(trip_duration) FROM df WHERE pickup_longitude < -73.95'
    ).df()

>> Function find_avg_trip_duration_in_the_west took 0.05432271957397461 seconds to run.

>> |    |   avg(trip_duration) |
>> |---:|---------------------:|
>> |  0 |              9.995189|

如你所见,使用 DuckDB API 的速度比原生 Pandas API 快十倍。

DuckDB 与 SQLite 的性能有何不同?

SQLite 是目前最受欢迎的内存数据库。这就是为什么我们在 Python 中默认提供它。

但是 DuckDB 在两个方面表现出色。首先,如果你不需要持久化,你也不必创建它。你可以像查询数据库表一样直接查询你的 Pandas dataframe。这就是我们在前一个示例中所做的。

## 6 个 Python GUI 框架,用于创建桌面、网页和甚至移动应用。

你可以纯粹用 Python 构建美丽的应用程序。

towardsdatascience.com

另一个好处是 DuckDB 仍然比 SQLite 更快。实际上,将本文中的示例复制到 SQLite 上运行的速度比 Pandas 慢。这使得 SQLite 在性能优先时成为最后的选择。但它仍然非常擅长持久化和移植数据及代码。

这是我们示例的 SQLite 版本:

...
import sqlite3

conn = sqlite3.connect("taxi.db")

df.to_sql("trips", conn)

@timing_decorator
def find_avg_trip_duration_in_the_west():
    cursor = conn.cursor()
    cursor.execute(
        "SELECT AVG(trip_duration) FROM trips WHERE pickup_longitude < -73.95"
    )
    result = cursor.fetchone()[0]
    cursor.close()
    return result

find_avg_trip_duration_in_the_west()

>> Function find_avg_trip_duration_in_the_west took 0.5150568962097168 seconds to run.
>> 9.995189

如你所见,在 Pandas 上运行 SQLite 的 SQL 需要我们在磁盘上创建一个单独的数据库实例,并将数据插入其中。如果我们使用 DuckDB,这些步骤都是不必要的。

此外,它花费了 0.51 秒,而 Pandas API 只需 0.49 秒。

结论

Pandas 毫无疑问是让 Python 成为数据科学热门选择的奇迹之一。然而,这个成熟的库使得数据整理任务变得缓慢。

在这篇文章中,我们讨论了使用 DuckDB 的简单查询 API 来提高速度。如果你了解 SQL,可以直接用 SQL 查询你的 Pandas 数据框,而不是使用 Pandas API。

尽管 DuckDB 有优点,但它并不是万灵药。

对于大规模项目,你仍然需要可扩展的数据库,比如 Postgres。而且,使用 Pandas API 可能开发更快,且所有懂 Python 的人都能理解它们。

感谢阅读,朋友!如果你喜欢我的文章,让我们在LinkedInTwitterMedium保持联系。

还不是 Medium 会员?请使用这个链接成为会员,因为对你没有额外费用,我会因推荐你而获得少量佣金。

测试你的智慧的 SQL 谜题

原文:towardsdatascience.com/sql-riddles-to-test-your-wits-8ce31202ae7f

时间戳、依赖过滤器和表现异常的左连接

Matt SosnaTowards Data Science Matt Sosna

·发布于 Towards Data Science ·阅读时长 8 分钟·2023 年 2 月 22 日

--

图片由 Saffu 提供,来源于 Unsplash

SQL 是一种看似简单的语言。通过其多种方言,用户可以使用类似英语的语法查询数据库。你看到的就是你得到的……直到你发现不是。

我时不时会遇到一些查询,它们的结果与我预期的完全不同,这教会了我一些语言中的细微差别。我在这篇文章中汇编了三个最近的难题,并将它们以谜题的形式排列,使其更有趣。尝试在阅读每个部分的结尾之前找出答案!

我还包含了快速的 公共表表达式(CTEs) 来生成每个示例中的表格,因此你不需要尝试查询你公司的生产表格!但要真正熟练掌握 SQL,我实际上建议你创建自己的数据库和表格进行练习。查看 这篇文章 了解如何操作。

请注意,所有查询均为 Postgres 语法 —— 在其他方言中你可能会得到不同的结果。最后,必须说明的是,每个查询中的实际数据和主题仅为示例。 🙂

图片由 Akram Huseyn 提供,来源于 Unsplash

谜题 1:时间戳的具体性

假设我们有一个名为 purchases 的表,其中包含购买 ID、金额以及购买时间。假设它看起来是这样的:

图片由作者提供

作为 CTE,它大致看起来像这样。请注意,我们需要指定dt列是时间戳,以免被解释为字符串。我们只需要为其中一行指定数据类型;其余的会被推断。

WITH purchases(id, amount, dt) AS (
    VALUES
    (1::bigint, 0.99::float, '2023-02-15 00:00:00 GMT'::timestamp),
    (2, 9.99, '2023-02-15 07:15:00 GMT'),
    (3, 15.99, '2023-02-15 23:01:15 GMT'),
    (4, 7.99, '2023-02-16 14:22:09 GMT')
)
...

现在让我们计算 2 月 15 日的购买总额。我们可以写一个如下的查询:

...
SELECT
    SUM(amount) AS sum
FROM purchases
WHERE
    dt = '2023-02-15'

我们神秘地收到以下响应。

作者提供的图片

发生了什么?2 月 15 日有三笔购买:ID 1、2 和 3。总和应该是$26.97。然而,只计算了第一笔购买。

提示

如果你将过滤器更改为2023-02-16,则没有行返回。

答案

dt列格式是包含日期和时间的时间戳。我们的WHERE过滤器只指定了日期。Postgres 不会拒绝此查询,而是自动将日期字符串重新格式化为2023-02-15 00:00:00。这仅匹配表中的第一笔交易,因此我们只计算了一行的总和。

如果我们想选择对应于 2 月 15 日的所有行,我们应该首先将时间戳转换为日期。

SELECT
    SUM(amount) AS sum
FROM purchases
WHERE
    DATE(dt) = '2023-02-15'

我们现在得到了预期的结果。

作者提供的图片

照片由Womanizer Toys提供,来源于Unsplash

谜题 2:依赖过滤器与独立过滤器

好的,下一个谜题。我们有一个名为users的表,我们的目标是删除符合任意一个三种条件的所有行。在下表中,例如,假设我们只想返回有职位并且活跃的用户,即那些在过去 28 天内登录过,曾经发过帖,并且不是新账户。

作者提供的图片

换句话说,我们希望我们的查询仅使用 8 号用户,该用户在no_login_l28has_never_postedis_new_account上都有 False 值。

让我们从查询的顶部开始。

WITH users(id, no_login_l28, has_never_posted, is_new_account) AS (
    VALUES
    (1, True, True, True),
    (2, True, True, False),
    (3, True, False, True),
    (4, True, False, False),
    (5, False, True, True),
    (6, False, True, False),
    (7, False, False, True),
    (8, False, False, False)
)
SELECT
    id
FROM users
WHERE
    ...

我们应该如何构建查询的WHERE子句?考虑一下——我们需要小心不要返回任何列为 **False**的行

当你准备好时,查看下面的选项。两个是正确的,两个是错误的。

选项 1:多个 **AND NOT**

WHERE
    NOT no_login_l28
    AND NOT has_never_posted
    AND NOT is_new_account

选项 2:多个 **OR NOT**

WHERE
    NOT no_login_l28
    OR NOT has_never_posted
    OR NOT is_new_account

选项 3: **NOT** + 分组 **OR**

WHERE
    NOT (
        no_login_l28
        OR has_never_posted
        OR is_new_account
    )

选项 4: **NOT** + 分组 **AND**

WHERE
    NOT (
        no_login_l28
        AND has_never_posted
        AND is_new_account
    )

提示

条件在过滤器中是分别评估还是一起评估?如果它们一起评估,我们能否将所有条件浓缩为一个TrueFalse值?

答案

选项 1. 这个让我有点困惑。我的团队中的一位数据科学家提交了一个包含这个过滤器的 PR,我确信它会提取行 2–7,因为查询只会移除所有三个列的值为 False 的用户。但令我惊讶的是,选项 1 实际上有效 因为三个过滤器是独立评估的。

选项 2. 这是我最初认为正确的过滤器,因为我没有意识到这些过滤器会被独立评估。但实际上这个过滤器会返回用户 2–8,因为任何在 no_login_l28has_never_postedis_new_account 中至少有一个 True 的用户都会被允许通过。 ❌

选项 3. 这是我最初认为过滤器需要这样表达的方式。如果用户在 no_login_l28has_never_postedis_new_account 中有 任何 一个 True,那么第 3 行到第 5 行评估为 TrueNOT 将其翻转为 False,这些行最终会被排除。这确实有效,我发现这比选项 1 更容易理解,但两者都是有效的。 ✅

选项 4. 这会返回与选项 2 相同的错误结果。第 3 行到第 5 行仅对用户 1 评估为 True,这意味着当我们用 NOT 取反时,所有剩余用户都会被提取出来。 ❌

Nick FewingsUnsplash 上的照片

谜题 3:左连接像内连接一样工作

看一下下面的查询。我们有两个表,customersreviewscustomers 包含客户 ID 及其在平台上花费的终身金额。

作者提供的图片

reviews 包含客户留下的评论信息:评论 ID、客户 ID 和评论是否被报告为垃圾评论。

作者提供的图片

这是生成两个 CTE 的子查询:

WITH customers(id, total_spend) AS (
    VALUES
    (100, 1583.49),
    (200, 8739.03),
    (300, 431.00),
    (400, 1.00),
    (500, 22.27)
),
reviews(id, customer_id, reported_as_spam) AS (
    VALUES
    (1, 100, False),
    (2, 100, False),
    (3, 400, True),
    (4, 400, True),
    (5, 500, False)
)
...

现在假设我们对客户的总消费与他们写的非垃圾评论数量之间的关系感到好奇。由于不是每个客户都留下了评论,我们希望将 reviews 左连接到 customers。我们可以这样构建我们的查询:

...
SELECT
    c.id,
    c.total_spend,
    COALESCE(COUNT(r.id), 0) AS n_reviews
FROM customers c
LEFT JOIN reviews r
    ON c.id = r.customer_id
WHERE
    NOT r.reported_as_spam
GROUP BY
    1, 2
ORDER BY
    1

准备好了吗?看看结果吧。

作者提供的图片

等一下。用户 200、300 和 400 去哪里了?为什么它们被移除了,我们怎么能把它们找回来呢?

提示

如果你创建一个过滤掉垃圾评论的 reviews CTE,然后在这个 CTE 上进行连接,我们会得到相同的结果吗?

答案

仔细查看,我们可以看到用户 200 和 300 从未留下任何评论。400 只有垃圾评论,但它们也被完全移除。由于我们进行了左连接,这些用户仍应存在于表中,并且 n_reviews 应为 0。相反,我们的左连接 表现得像内连接

问题是,**WHERE** 子句是在连接操作之后进行评估的。我们的左连接带来了用户 200 和 300 的reported_as_spam的空值。然后,WHERE过滤器移除所有reported_as_spam为 True 的行,这样用户 400 就被移除。然而,这个过滤器也会移除空值,因此用户 200 和 300 也被移除。

为了正确完成这一点,我们需要在与customers连接之前预先过滤reviews。正如提示所述,我们可以为reviews创建一个 CTE,并在那里进行过滤。但更有效的是,我们可以在连接内部进行过滤

我们可以通过在LEFT JOIN块中添加AND NOT r.reported_as_spam来实现。见下文:

...
SELECT
    c.id,
    c.total_spend,
    COALESCE(COUNT(r.id), 0) AS n_reviews
FROM customers c
LEFT JOIN reviews r
    ON c.id = r.customer_id
    AND NOT r.reported_as_spam
GROUP BY
    1, 2
ORDER BY
    1

现在我们得到了预期的结果。

作者提供的图像

Laura Chouette拍摄,来源于Unsplash

结论

本文分享了三种可能导致意外结果的 SQL 难点:时间戳的具体性、依赖性与独立性过滤器,以及左连接表现得像内连接。我特别提供了简单的示例,以保持对语法的关注,但你可能会在大型复杂查询中遇到类似的 SQL 细微差别。

这些错误可能非常难以识别,尤其是对于包含多个组件的查询。当我对结果感到困惑时,我会尝试将查询拆分成各个部分,并验证每个组件的结果。但是如果有疑问,写一些简单的 CTE 并用测试数据进行验证,确认结果是否符合预期。

祝查询愉快!

Matt

SQL 用户:使用这一技巧将你的查询长度缩短一半

原文:towardsdatascience.com/sql-users-halve-the-length-of-some-of-your-queries-with-this-one-trick-c3c2c226cb35

QUALIFY 子句纯粹是语法糖

Matt ChapmanTowards Data Science Matt Chapman

·发布于 Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 29 日

--

图片由 Joanna Kosinska 提供,来源于 Unsplash

在这篇文章中,我将向你介绍 SQL QUALIFY 子句:一种帮助你编写更简洁、可读性更高的 SQL 查询的技巧。

如果你从事数据科学/分析或机器学习,这是一个必备的 SQL 工具,原因有四:

  1. 首先,因为它可以帮助你编写更 简短 的查询,简化你的数据收集流程。在我的经验中,QUALIFY 将许多查询的长度减少了多达 50%。

  2. 其次,因为它会帮助你编写更 简洁 的查询。这很重要,因为正如保罗·格雷厄姆所说,简洁就是力量,它使你的代码更具可读性和可维护性。

  3. 第三,QUALIFY 帮助你编写更 高效 的查询,从而降低成本并加快开发速度。

  4. 第四,它是一个展示技巧的好方法。我并不是说你会因为使用 QUALIFY 赢得任何创新奖,但因为它是一个 相对较新的子句,你的老板可能还没有听说过。无论如何,展示给你的团队一个新技巧会很有趣。

QUALIFY 子句的作用是什么?

简单来说,QUALIFY 子句使你可以筛选窗口函数的结果,无需 使用嵌套子查询或 WITH 子句。它允许你用 一行代码 替代这些冗长的代码块。这有点类似于 HAVING 子句如何使你能够筛选 GROUP BY 聚合的结果。

QUALIFY 子句使你能够筛选窗口函数的结果

如果这听起来像胡言乱语,给我 2 分钟的时间,我会尽力解释清楚。

一切从窗口函数开始

如果你不熟悉窗口函数,你可能想看看这篇文章,它通过 4 个实际使用案例介绍了 SQL 窗口函数。但简而言之,窗口函数可以用来对表中的一组行进行计算,而无需聚合或合并这些行,这在你想保留表的原始结构时非常有用。

例如,假设我们有一个名为orders的表,它列出了公司收到的所有产品订单。每一行代表一个订单,并记录了订单下达的date、下订单的customerorder_amount(以美元计)。

图片来源于作者

使用窗口函数,我们可以添加一个新的列total_order_amount,表示每个客户的总订单金额。我们可以简单地写:

SELECT
  date,
  customer,
  order_amount, 
  SUM(order_amount) OVER(PARTITION BY customer) AS total_order_amount
FROM orders
ORDER BY date

这将返回:

图片来源于作者

如你所见,窗口函数SUM(order_amount) OVER(PARTITION BY customer)有效地将我们的表分区为不同的“窗口”(每个客户一个窗口),然后计算这些窗口的total_order_amount。所有这些都在不使用GROUP BY聚合的情况下完成,使我们能够保留相同数量的行。

输入 QUALIFY 子句

现在我们已经设置好了窗口函数,假设我们想向我们花费最多的客户发送感谢邮件。在此之前,我们需要筛选这个表,查看total_order_amount超过$5,000 的客户订单。换句话说,我们想生成这样的内容:

图片来源于作者

一种生成这个结果的典型方法是使用WITH子句:

WITH my_order_table AS (
  SELECT
    date,
    customer,
    order_amount, 
    SUM(order_amount) OVER(PARTITION BY customer) AS total_order_amount
  FROM orders
)

SELECT
  *
FROM my_order_table
WHERE total_order_amount > 5000
ORDER BY date

…或者一个嵌套子查询:

SELECT
  *
FROM 
    (SELECT
      date,
      customer,
      order_amount, 
      SUM(order_amount) OVER(PARTITION BY customer) AS total_order_amount
    FROM orders) AS A

WHERE A.total_order_amount > 5000
ORDER BY A.date

然而,虽然这两种方法都是完全可接受的解决方案,但QUALIFY子句使我们能够显著简化和缩短这段代码。我们可以简单地写:

SELECT
  date,
  customer,
  order_amount, 
  SUM(order_amount) OVER(PARTITION BY customer) AS total_order_amount
FROM orders
QUALIFY total_order_amount > 5000

这将返回完全相同的结果

QUALIFY 语句是纯粹的语法糖

QUALIFY 子句的一个好处是它在窗口函数计算之后被评估。这意味着你可以使用你分配给窗口函数列的别名进行引用。在上面的例子中,我通过编写QUALIFY total_order_amount..演示了这一点,引用了我在创建原始窗口函数时分配的别名total_order_amount

图片来源于作者

SELECT 子句中不必写窗口函数。例如,如果我不想实际将总订单金额显示为单独的列,但仍需在这个窗口函数上进行过滤,我可以这样写:

SELECT
  date,
  customer,
  order_amount, 
FROM orders
QUALIFY SUM(order_amount) OVER(PARTITION BY customer) > 5000

这将返回相同的结果,但没有 total_order_amount 列:

作者提供的图片

除此之外,QUALIFY 子句也可以比使用子查询或 WITH 稍微更高效。这不仅会加快你的开发时间;它还对成本优化有很大帮助。

另一个例子:选择每组中的前 N 名

QUALIFY 子句的一个常见用例是选择每组中的前 N 名。例如,假设我们有一个记录每个商店在某一天进行的每笔销售的 sales 表。每一行代表一次交易,并包含销售的时间戳、购买的产品以及该笔销售中花费的总金额。

使用窗口函数:

SELECT
  Timestamp,
  Product,
  Sale_value,
  ROW_NUMBER() OVER(PARTITION BY Product ORDER BY Timestamp ASC) as row_num
FROM sales
ORDER BY Product, row_num

我们可以在每个产品类别内给每笔销售分配一个行号:

作者提供的图片

然后,使用 QUALIFY 语句,我们可以过滤结果,只显示每个类别中的第一行:

SELECT
  Timestamp,
  Product,
  Sale_value,
  ROW_NUMBER() OVER(PARTITION BY Product ORDER BY Timestamp ASC) as row_num
FROM sales
QUALIFY row_num = 1
ORDER BY Product

作者提供的图片

就是这样——QUALIFY 的魔力。

最后一个事情

QUALIFY 子句在所有 SQL 方言中尚不可用。在撰写时,它已被许多大型平台如 BigQuery、Databricks、Snowflake 和 H2\ 支持。如果你的平台/方言不兼容,你现在需要依赖子查询/WITH,但你可以关注现代 SQL以查看它何时可用。

利用 SQL 中的分析函数加速数据提取

原文:towardsdatascience.com/sql-window-functions-939ed24c9752

分析函数提供了一种极其强大且易于实现的数据处理和分析方式。本文将展示如何在 SQL 语句中使用分析函数。

Günter RöhrichTowards Data Science Günter Röhrich

·发布在 Towards Data Science ·8 分钟阅读·2023 年 7 月 18 日

--

作为一名分析师,你很可能会遇到需要查询数据以进行分析的情景。数据通常来自 SQL 数据库,然后通过像 Python 这样的编程语言和 Pandas 或 NumPy 等强大框架导入。这是一种处理数据的完全有效的管道,但重的计算主要依赖于你的本地机器。对于小数据集,这不是问题,但对于较大的数据集,可能会遇到仅依靠 PC 本地内存处理大量数据的困难。

你可能认为这不是一个常见的问题。因此,让我给出一个日常的例子来证明这个假设是错误的:

想象一下,你在一家制造公司工作,并且对收集机器传感器数据感兴趣。这些数据通常被频繁收集,而且可能非常嘈杂。为了更好地理解你的机器状态,对密集收集的数据进行平滑和预处理(例如,测量可能每秒收集几次)很快就会产生庞大的数据集!假设我们在一台机器旁边放置了 150 个传感器,每个传感器每秒读取 4 次数据。那么一天的数据将会产生

4×60×60×24x150 ≈ 52 百万条记录(读取次数 × 秒 × 分钟 × 小时 × 传感器)

数据点。通常,作为经验法则,我们会查看至少一周的数据(但我们也可能增加读取次数或传感器数量)……你能理解这点。

因此,你可能更好地将计算成本高的聚合操作转移到源数据库中。特别是,分析或窗口函数的语法直接明了,却是读取、转换和提取数据的强大工具,适用于更高层次的聚合。

关键要点

每当你看到需要滚动/移动窗口或在逻辑分区内进行计算(例如,持续排名,从最低到最高值,在某些传感器组内),直接在 SQL 查询中应用窗口函数通常是值得考虑的,而不是使用 Pandas 中实现的更昂贵的函数来实现类似的功能。

这篇简短的文章是如何结构化的?

  1. 一个 简短的介绍 例子和窗口函数如何工作的理论

  2. 一个 快速指南来安装一些 SQL 演示数据 以及一个可以在电脑上直接使用的数据库(这很简单!)。我们将使用著名的 纽约出租车 数据。

  3. 示例 让你熟悉这些函数

一些窗口函数的 Julio César Bosch

介绍

为什么你需要窗口函数?

在数据科学更具吸引力的领域引起的轰动中,我们常常忽视了数据科学家大部分时间花费在获取、处理和清洗数据上。每当你看到需要计算成本高的运行总和或移动平均时,你应该考虑将这些转换操作转移到数据库中。

这种 函数是对一系列语境上相关的行进行操作。通常,当涉及此类事务时,我们会考虑总和、平均值、最小值或最大值计算。

窗口函数一般是如何结构的?

首先,了解使用窗口函数的查询结构,我们来看一个使用滚动总和的 SQL 语句——只需查看代码块,不需要完全理解概念,暂时把代码当作简单演示:

select
   VendorID,
   tpep_dropoff_datetime as dropdate,
   passenger_count,
 sum(passenger_count) over (
     partition by date(tpep_dropoff_datetime)
     order by tpep_dropoff_datetime
     rows between unbounded preceding and current row
 ) as day_running_sum
from trip_data td 
  1. 首先,我们指定我们希望执行的分析类型。一般来说,有三种类型的函数 可供使用:
  • 聚合函数,如 min()、max()、count() 或 avg()——这在上面的第一个示例中展示过

  • 编号函数,如 rank() 或 row_number()

  • 导航函数 如 lead()、lag() [返回后续或前值]

2. 在下一步中,我们开始考虑 逻辑分区。例如,我们可以选择用户 ID 作为分区示例,因此我们查看用户块,并对每个用户子集执行进一步操作。必须将分区传递给函数!

3. 类似于 group by 语句,我们还应在分析函数中考虑排序参数。排序应用于之前定义的选定子块。排序是可选的,可以省略。

  1. 最后,分析(或窗口)函数的核心元素:窗口框架子句。这个子句指定了我们希望查看的滚动时间窗口。

要深入了解更多理论,请访问下面的链接 — 请注意,窗口函数在不同的 SQL 数据库中可能会有所不同,但它们的一般结构基本相同。

[## 窗口函数

窗口函数是 SQL 函数,其中输入值来自一个或多个行的“窗口”中的...

www.sqlite.org

快速安装指南

为了说明窗口函数如何工作,我将提供一些使用著名的纽约出租车数据集的子集的示例。通常,你需要以下工具来自己完成这些示例:

  • 已安装 Python(用于提取,需 sqlite3 和 pandas 库)

  • 一种编写查询和查看数据的工具(谈到 SQL,DBeaver 是一个功能强大的数据查询工具,但易于使用且开源。

就这样!

如果你想复制所展示的步骤,只需访问下面的链接并下载即可:

[## TLC 行程记录数据

黄绿出租车的行程记录包括捕捉接送日期/时间的字段、接送...

www.nyc.gov

法律说明: 只要使用目的合法 ,数据使用没有限制,如使用条款中所述。更多许可证详细信息和数据集引用请见* 文章结尾*。

下载成功后,我建议将 parquet 文件转换为 sqlite3 数据库。使用 Python 以及 Pandas 和 sqlite3 包,这很快就能实现——只需将代码复制粘贴到脚本中,你就准备好了:

import pandas
import sqlite3

data = pd.read_parquet('yellow_tripdata_2023-01.parquet')

con = sqlite3.connect('taxi.db')  # creates a database "taxi.db"
data.to_sql('trip_data', con)     # name of the table

完成了!我们现在可以在存储在硬盘上的非常简单的 SQL 数据库中工作了。

示例

让我们从获取整个数据的费用总和和平均值这一简单任务开始。为此,我们可以使用 SUM 和 AVG 参数,并在 over 语句后的括号中省略分组标准。由于我们只是对整个数据集进行聚合,因此尚无需使用窗口函数。

SELECT  
       SUM(fare_amount) AS total_fare,
       AVG(fare_amount) AS average_fare
FROM trip_data td;

假设我们想了解每一天每个供应商(VendorID)的总费用和平均费用——这个任务会变得稍微有些挑战:

SELECT
 VendorID,
 date(tpep_dropoff_datetime) as dropdate,
 SUM(fare_amount) AS total_fare,
    AVG(fare_amount) AS average_fare
FROM trip_data td
group by VendorID, dropdate

如我们所见,为了实现分组效果,我们需要使用分组关键字,使我们能够根据VendorIddropdate进行汇总。虽然分组非常强大,但它不能进一步将单独行拆分为拖尾总和或移动平均值——这时窗口函数就派上用场了。

现在假设我们想计算fare_amount的滚动总和和均值,并按VendorID和相关的每日行程进行分类。

我们的窗口操作在我们指定的“分区”内。我们定义一个分区为整天,因此,我们期望看到每一天和每个供应商的滚动总和和均值(或等效地,在每个分区内)。

select
   VendorID,
   date(tpep_dropoff_datetime) as dropdate,
   fare_amount,
   sum(fare_amount) over (
      partition by date(tpep_dropoff_datetime), VendorID 
      order by tpep_dropoff_datetime
      rows between unbounded preceding and current row
   ) as rolling_sum,
   avg(fare_amount) over (
      partition by date(tpep_dropoff_datetime), VendorID 
      order by tpep_dropoff_datetime
      rows between unbounded preceding and current row
 ) as rolling_mean
from trip_data td;

每个分区(dropdate + VendorID)显示每行的票价以及滚动总和。

注意以下事项,我们使用了“unbounded preceding”作为框架子句。这意味着我们查看在我们分区内的所有前置值——再说一次,我们的分区定义为Date + VendorID

如果我们只想查看一个滚动总和/均值,该均值考虑到之前和当前的票价(再次在Date+VendorID的分区内),我们需要将框架子句调整为仅 1 行之前和当前行

select
 VendorID,
 date(tpep_dropoff_datetime) as dropdate,
 fare_amount,
 round(sum(fare_amount) over (
  partition by date(tpep_dropoff_datetime), VendorID 
  order by tpep_dropoff_datetime
  rows between 1 preceding and current row --> NOW 1 Preceding row only
 ),2) as rolling_sum,
 round(avg(fare_amount) over (
  partition by date(tpep_dropoff_datetime), VendorID 
  order by tpep_dropoff_datetime
  rows between 1 preceding and current row --> NOW 1 Preceding row only
 ),2) as rolling_mean
from trip_data td 

“1 Preceding”结果是一个移动总和/均值,仅查看当前和之前的行程票价——再说一次,这始终在分区内。

在最后一个示例中,我们将结合所有之前的步骤,展示如何优雅地查询简单的汇总数据,并使用这些数据执行 rank()语句——这是一种分析编号函数。

我们希望按每个供应商收取的总票价的最高值对天数进行排名。因此,我们将这分为两个步骤:

  1. 通过简单的“group by”按天和供应商汇总票价

  2. 使用分析函数对“VendorID”分区内的天数进行排名

with aggregated as (
 select
    VendorID,
    date(tpep_dropoff_datetime) as dropdate,
    round(sum(fare_amount),2) as total
 from trip_data td 
 where dropdate like '2023%' -- this to remove wrong 2020 data in the dataset
 group by VendorID, dropdate
 order by dropdate asc
) -- here our "aggregated" data subset is created
select 
  VendorID,
  dropdate,
  total,
  rank() over (
      partition by VendorID
      order by total desc
  ) as best_days
from aggregated -- we rank() based on "aggregated"

VendorID=1开始,1 月份最好的日子是 26 号,它排名第一(收入最高)。

结论

到现在为止,我相信你已经掌握了分析函数的工作原理以及它们如何灵活地实现相当复杂的查询结果。

虽然数据现在通常在本地机器或云实例上转换,但在实际导入到后续软件之前修改数据通常是更好的选择——也就是说,SQL 是一种非常强大的语言来完成这项任务!

如果你觉得这篇文章对你有帮助,我会很感激你的“关注”🫀,直到那时:

{照顾好自己,如果可以的话,也照顾好别人}

—— 借用自斯蒂芬·杜布纳

数据集引用和使用条款:

纽约市出租车和豪华轿车委员会(TLC)的行程记录数据于 7 月 13 日从www.nyc.gov/site/tlc/about/tlc-trip-record-data.page访问。

完整的使用条款可以在这里找到:

[## 使用条款

欢迎访问纽约市官方网站。在使用之前,请仔细阅读以下使用条款(“条款”)…

www.nyc.gov](https://www.nyc.gov/home/terms-of-use.page?source=post_page-----939ed24c9752--------------------------------)

SquirrelML:预测纽约中央公园的松鼠接近行为

原文:towardsdatascience.com/squirrelml-predicting-squirrel-approach-in-nycs-central-park-8c3719d8ff65

通过机器学习探索纽约中央公园的松鼠行为:聚类观察和预测互动见面

Victor MurciaTowards Data Science Victor Murcia

·发表于Towards Data Science ·18 分钟阅读·2023 年 12 月 20 日

--

照片由Tj Holowaychuk拍摄,发布于Unsplash

NYCOpenData拥有丰富的有趣数据集,涉及健康、环境、商业和教育等话题。我偶然发现了2018 Central Park Squirrel Census数据集,并立即决定要对其进行一些处理。该数据集记录了志愿者在两周内在中央公园收集的松鼠观察数据。浏览数据字典后,我被一个名为‘Approaches’的特征吸引,该特征表示是否观察到松鼠接近人类。我认为训练一个机器学习(ML)模型来帮助我判断位于中央公园范围内的松鼠是否会接近我会很有趣。本文将详细介绍这个周末项目的整个过程。这个项目涉及许多内容:地理空间数据处理、聚类、可视化、特征工程、非结构化文本、模型训练、模型校准和模型部署。

我在 streamlit 应用中部署了该模型,你可以输入你的坐标和其他特征,这将告诉你松鼠接近你的概率。你可以在这里玩这个应用。如果你对查看一些代码感兴趣,我已将.ipynb 文件发布在这里

数据加载和初步 EDA

数据加载相当标准。

ini_squirrel_df = pd.read_csv('/content/drive/MyDrive/SquirrelML/NYC_Squirrels.csv')

为了进行初步的 EDA,我使用了 dataprep 来快速了解原始数据集中存在的特征分布、基数、模式、缺失数据和相关性。你可以查看报告这里。从中获得了几个有用的见解,这些见解帮助我规划后续的特征工程,并移除冗余/不必要的特征。以下是我从这次 EDA 中获得的一些显著观察结果:

  • 数据集由 3023 个独特观察值、31 列组成,其中 13%的单元格缺失。

  • 大多数特征是分类的和布尔值的。

  • 纬度和经度条目似乎有一个大致的四峰分布

  • “移位”列相当平衡(~55%的观测是在下午/晚上进行的)

  • “漠不关心”列相当平衡(~51%的条目为 False)

  • 大多数松鼠是成年松鼠,主要毛色为灰色

  • 观察到的最常见的位置是树木

  • 颜色备注、具体位置、其他活动和其他互动是有大量缺失数据的文本列

  • “接近”高度不平衡(94.11%的观测值为负)

目标变量‘接近’的巨大类别不平衡。图片由作者提供

这些都是有用的观察,因为现在我知道我尝试解决的问题是一个不平衡的二分类问题。此外,我现在知道使用聚类方法对松鼠观测进行分组可能在未来会有用。此外,文本列可能包含可以创建新、潜在有用特征的信息。

松鼠观测的聚类

如下所示,松鼠观测似乎具有大致的三峰或四峰分布。我认为尝试根据位置对松鼠观测进行分组会很有趣。

中央公园范围内松鼠观测的经度和纬度。图片由作者提供。

虽然我们可以通过轮廓系数或肘部法则来尝试确定最佳的簇数,但在这种情况下,我想将观测结果简单地划分为 4 个区域。我使用纬度和经度运行了 K 均值算法,如下所示:

# Function for Applying Clustering
def apply_clustering(df, num_clusters=4):
    kmeans = KMeans(n_clusters=num_clusters, n_init='auto')
    df['Cluster'] = kmeans.fit_predict(df[['X', 'Y']])
    centroids = kmeans.cluster_centers_

    # Assuming 'kmeans' is your trained KMeans model
    joblib.dump(kmeans, '/content/drive/MyDrive/SquirrelML/squirrel_kmeans.pkl')

    return df, centroids

K-Means 算法的结果如下所示。有一些明显的边界,我通过 Voronoi 图进行了强调。你还可以看到每个簇的质心通过红色 X 标记出来。我还使得那些接近松鼠的点没有被包围在暗圈中,而那些没有接近的点则被包围在内。

松鼠普查数据集的纬度和经度上的 K-Means 聚类结果。图片由作者提供。

松鼠观测的地理空间可视化

地理空间数据处理起来很有趣。如果你查看中央公园的鸟瞰图,或者对其布局比较熟悉,你就会看到布局的逐一对应。例如,中央公园有三个主要的水体,这些水体出现在图上的空白区域。你可以在这里找到中央公园的标注布局。例如,位于‘黄色’簇中的那个大‘空旷’区域实际上是‘水库’。右上方,我们还可以看到一个接近空白的区域,对应于‘哈莱姆湖’。最后,在蓝色簇的左下角,我们可以看到被称为‘湖泊’的水体。

为了更好地利用这些数据,我认为将这些观测结果直接可视化在地理空间准确的纽约市地图上会很有趣。为此,我再次转向 NYCOpenData,并从这里下载了纽约市的 shape (.shp)文件。你的下载应该包括 4 个不同的文件:一个.shp 文件、一个.prj 文件、一个.dbf 文件和一个.shx 文件。在加载这些文件之前,请确保所有文件都在同一个目录中。下载这些文件后,我用 Geopandas 将它们加载到环境中。这些文件通常比较大,加载可能需要几分钟(在我的情况下大约需要 5 分钟)。

nyc_map = gpd.read_file('geo_export_0a3d2fab-a76c-40da-bc23-6e335dd753dd.shp')

为了使.shp 文件更加轻量化并提高工作速度,我使用 Geofeather 将 Geopandas 框架转换为 feather 对象。

geofeather.to_geofeather(nyc_map,'nyc_map.feather')

现在,我可以从 feather 文件中重新生成 nyc_map 并绘制它,这比使用原始的.shp 文件要快很多。我们可以如下绘制 nyc_map:

nyc_map = geofeather.from_geofeather('nyc_map.feather')
nyc_map.plot()

结果如下:

地理空间准确的纽约市地图。图片由作者提供。

看起来很棒!你还可以在纽约市陆块的左上角看到中央公园。尽管这个视图很酷,但我想更多地关注中央公园区域。因此,我也不需要整个数据集,只需根据松鼠观测坐标进行过滤。这还将加速所有后续操作,因为我们将处理纽约市地图的一个更小的子集。

质心是形状的几何中心。在地理空间数据的上下文中,它指的是地理特征的中心点。这个点是形状中所有点的平均位置。对于简单的形状如多边形(例如城市或公园的边界),质心通常位于形状内部。对于更复杂的形状,尤其是那些有凹部的形状,质心可能位于形状的物理边界之外。有几种方法可以用来计算质心。这里我只是使用了 shapely 包和 nyc_map:

from shapely.geometry import Point
# Calculate the centroid for each geometry
nyc_map['centroid'] = nyc_map.geometry.centroid

然后,我可以使用松鼠目击的坐标范围来过滤 nyc_map。

# Filter based on centroid's longitude and latitude
filtered_nyc_map = nyc_map[(nyc_map['centroid'].x >= -74.0) &
                           (nyc_map['centroid'].x <= -73.94) &
                           (nyc_map['centroid'].y >= 40.75) &
                           (nyc_map['centroid'].y <= 40.82)]

现在我们可以绘制过滤后的纽约市地图,如果我们做对了的话,它应该以中央公园为中心。

# Plot the filtered DataFrame
filtered_nyc_map.plot(figsize=(10, 10), aspect='auto')

结果如下:

纽约中央公园的地理空间准确图。图片由作者提供。

成功了!不过,我们还有一件事要做。我想在这个图上包括按集群着色的松鼠目击情况,并根据松鼠的接近程度进行强调。我们可以使用下面的代码来实现:

# Convert the 'X' and 'Y' columns into a GeoSeries of Points
points = gpd.GeoSeries([Point(xy) for xy in zip(squirrel_df['X'], squirrel_df['Y'])])

# Create a new GeoDataFrame
squirrel_geo_df = gpd.GeoDataFrame(squirrel_df, geometry=points)

# Plotting the filtered NYC map
fig, ax = plt.subplots(figsize=(10, 10))

filtered_nyc_map.plot(ax=ax, color='#207388')  # Base map in light grey

# Overlay the squirrel data points, color-coded by 'Cluster'
squirrel_geo_df.plot(ax=ax, column='Cluster', categorical=True, markersize=20,
                     cmap='viridis', legend=True, legend_kwds={'title':'Cluster', 'loc': 'upper left'},
                     facecolors='none' if True else 'black', edgecolors='black')

# Adding labels and title
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
ax.set_title('Squirrel Sightings in Central Park by Cluster')
plt.show()

这个结果如下:

按集群着色的松鼠目击情况,在地理空间准确的纽约中央公园地图上。图片由作者提供。

这真不错!我对这个结果非常满意。我还想做一个可视化。我找到了中央公园的一张非常酷的航拍照片。我认为将松鼠目击情况覆盖到航拍视图上会很有趣,但是,有一些其他效果使得绘图变得复杂,所以我决定将这个任务留到以后再做。

特征工程

正如我在介绍中提到的,这个数据集有很多特征工程的机会。我创建的大多数新特征都是从文本列派生出来的。这些文本列包含了志愿者们对每次松鼠目击的额外描述。我识别出了一些关键类别和与每个类别相关的术语,并结合正则表达式创建了如下的新特征:

# Define lists of terms for various features
tree_terms = ['tree', 'trees', 'maple', 'log', 'branch', 'oak', 'willow', 'trunk', 'stump', 'elm']
shrubbery_terms = ['shrub', 'bush', 'weeds']
rock_terms = ['rock', 'stone']
grassland_terms = ['lawn', 'grass', 'field', 'glen']
path_terms = ['path', 'road', 'pavement', 'street']
structure_terms = ['conservatory', 'fountain', 'bench', 'fence', 'statue', 'bin', 'carousel', 'trellis',
                  'structure', 'car', 'table', 'ledge', 'railing', 'overpass', 'post', 'can', 'house',
                  'arch', 'bar', 'sanctuary', 'bridge', 'bike', 'rack', 'construction', 'playground']
water_terms = ['water', 'shore', 'pond', 'pool', 'stream']

# Function to create a matching column based on specified terms
def create_matching_column(df, column_name, source_column, terms):
    pattern = '|'.join(terms)
    df[column_name] = df[source_column].str.contains(pattern, case=False, na=False)
    return df

在上面的例子中,我能够创建出像‘树上出现’、‘灌木丛中出现’和‘岩石上出现’等新特征。除此之外,一些列如‘主要毛色’和‘突出毛色’等被进行了独热编码,因为它们有几个类别(例如,主要毛色包括灰色、黑色、肉桂色和未知)。还有其他特征,如果你对查看完整的 EDA 报告以及查看创建的所有特征感兴趣,可以查看这里的报告。

预处理数据集的各种特征的依赖图。图片由作者提供。

我创建了一个管道来进行所有必要的清理、格式化等,这是我在处理数据集后决定必要的。你可以查看我在文章开头链接的笔记本中的所有工作。值得一提的几点是:

  • 我们现在有 44 个特征(原始数据集有 31 个)。

  • 没有删除任何观察值,因此我们仍然有 3023 行。

  • 现在没有缺失数据了。没有进行插补或删除行。大部分缺失数据出现在文本列中。这些列在用于特征工程后被删除。在像 Primary Fur Color 或 Age 这样的列中,我简单地将 NaNs 视为一个名为“未知”的新类别。

下面的相关图并没有特别有用。它主要揭示了我已经预料到的相关性/关系。例如,大多数强反相关是与 Primary Fur Color(即,灰色与肉桂色)或 Squirrel Age(即,成人与幼年)的变量之间的。

预处理数据集中特征的皮尔逊相关图。图像由作者提供。

此时,我想查看像主成分分析(PCA)和 t-分布随机邻域嵌入(tSNE)这样的降维技术,以尝试识别数据中的附加模式/簇,并评估哪些特征具有最重要的权重。tSNE 图并没有特别揭示出什么,尽管我可以在数据中看到一些分组。我也进行了 PCA,加载图和解释方差图在告诉我 30 个特征占了大约 90%的方差方面非常有用,还可以看到像 Cluster、PFC_Gray、X、Y、PFC_Cinnamon 和 HFC_Gray 这样的特征是数据集中一些最强的成分。

降维技术和处理过的松鼠数据分析。图像由作者提供。

训练模型

从我决定采取机器学习方法的那一刻起,我就知道我只会考虑两个算法:决策树和随机森林。原因是……松鼠喜欢树,这个笑话是我决定做这个项目的主要原因。虽然其他算法可能表现更好,但它们的选择就没那么有趣了。如果你有任何有趣的建议,请告诉我 😃

无论如何,接下来我们开始建模。首先,这是我目前的数据框的视图。

清理、工程化和预处理的松鼠普查数据集。图像由作者提供。

首先,我们需要创建我们的训练集和测试集。在进行拆分时需要记住的一点是,我们必须使用目标变量进行分层拆分,因为数据集严重不平衡。分层拆分将保留每个类别的样本百分比。

# Assuming squirrel_df is your DataFrame and 'Approaches' is the target variable
X = squirrel_df.drop('Approaches', axis=1)  # Features
y = squirrel_df['Approaches']  # Target

# Stratify the split to maintain the class distribution in train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

鉴于数据集的不平衡性,我考虑使用数据增强技术。然而,我将这些内容留待未来的迭代,因为引入合成样本可能会带来其他复杂情况,而且根据我的经验,这些样本通常泛化效果不佳。

在此期间,我将通过在训练模型时使用平衡权重来处理不平衡问题。关于指标,我将关注接收者操作特征曲线下面积(ROC-AUC)得分和精确度召回曲线下面积(PR-AUC)得分。特别是 PR-AUC 得分在不平衡分类问题中很有用,因为它关注少数类,而 ROC 曲线则涵盖了两个类别。

我对决策树和随机森林模型进行了网格搜索。我使用了 5 折分层交叉验证,因为我想在分割间保留类别百分比。我还使用了 PR-AUC 和 ROC-AUC 作为超参数调优过程中的优化指标。对于随机森林,我查看了 n_estimators、max_depth、min_samples_split 和 min_samples_leaf,使用了保守的数值范围。随机森林超参数调优的代码如下所示:

from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import make_scorer, average_precision_score, roc_auc_score

# Define the parameter grid for Random Forest
rf_param_grid = {
    'n_estimators': [50, 100, 200, 500],  # Number of trees in the forest
    'max_depth': [None, 5, 10, 20],  # Maximum depth of the tree
    'min_samples_split': [2, 5, 7, 10],  # Minimum number of samples required to split a node
    'min_samples_leaf': [1, 3, 5, 10]  # Minimum number of samples required at each leaf node
}

# Initialize Stratified K-Fold
stratified_kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# Define multiple scoring metrics
scoring_metrics = {
    'PR-AUC': make_scorer(average_precision_score, needs_proba=True),
    'ROC-AUC': 'roc_auc'
}

# Initialize the GridSearchCV object with Stratified K-Fold
rf_grid_search = GridSearchCV(
    estimator=RandomForestClassifier(random_state=42, class_weight='balanced'),
    param_grid=rf_param_grid,
    cv=stratified_kfold,  # Using Stratified K-Fold here
    scoring=scoring_metrics,
    refit='PR-AUC',
    n_jobs=-1,
    verbose=2
)

rf_grid_search.fit(X_train, y_train)

# Best parameters and scores
print("Best parameters for Random Forest:", rf_grid_search.best_params_)
print("Best PR-AUC score:", rf_grid_search.best_score_)

# To access the results for each metric
cv_results = rf_grid_search.cv_results_

# Find the index of the best score based on the refit criterion
best_index = rf_grid_search.best_index_

# Display the best hyperparameters and corresponding scores
print("Best Hyperparameters:", rf_grid_search.best_params_)
print(f"Best Mean Test PR-AUC: {cv_results['mean_test_PR-AUC'][best_index]:.4f}")
print(f"Best Mean Test ROC-AUC: {cv_results['mean_test_ROC-AUC'][best_index]:.4f}")

对于决策树,我也使用了小范围的 max_depth、min_samples_split 和 min_samples_leaf 值。决策树超参数调优的代码如下所示:

from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.tree  import DecisionTreeClassifier
from sklearn.metrics import make_scorer, average_precision_score, roc_auc_score

# Define the parameter grid for Decision Tree
dt_param_grid = {
    'max_depth': [10, 20, None],  # Maximum depth of the tree
    'min_samples_split': [2, 5],  # Minimum number of samples required to split a node
    'min_samples_leaf': [1, 2]  # Minimum number of samples required at each leaf node
}

# Initialize Stratified K-Fold
stratified_kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# Define multiple scoring metrics
scoring_metrics = {
    'PR-AUC': make_scorer(average_precision_score, needs_proba=True),
    'ROC-AUC': 'roc_auc'
}

# Initialize the GridSearchCV object with Stratified K-Fold
dt_grid_search  = GridSearchCV(
    estimator=DecisionTreeClassifier(random_state=42, class_weight='balanced'),
    param_grid=dt_param_grid,
    cv=stratified_kfold,  # Using Stratified K-Fold here
    scoring=scoring_metrics,
    refit='PR-AUC',
    n_jobs=-1,
    verbose=2
)

dt_grid_search.fit(X_train, y_train)

# Best parameters and scores
print("Best parameters for Decision Tree:", dt_grid_search.best_params_)
print("Best PR-AUC score:", dt_grid_search.best_score_)

# To access the results for each metric
dt_cv_results = dt_grid_search.cv_results_

# Find the index of the best score based on the refit criterion
best_index = dt_grid_search.best_index_

# Display the best hyperparameters and corresponding scores
print("Best Hyperparameters:", dt_grid_search.best_params_)
print(f"Best Mean Test PR-AUC: {dt_cv_results['mean_test_PR-AUC'][best_index]:.4f}")
print(f"Best Mean Test ROC-AUC: {dt_cv_results['mean_test_ROC-AUC'][best_index]:.4f}")

评估和解释模型

在评估最佳随机森林和决策树模型在高度不平衡数据集上的表现时,我观察到了 ROC-AUC 和 PR-AUC 评分的明显差异。随机森林模型以 0.91 的 ROC-AUC 得分表现优异,显著超过了决策树 0.77 的体面得分。这表明其在区分类别方面的能力更强。然而,由于 PR-AUC 评分在不平衡数据集的背景下关注精确度与召回率的平衡,结果却有所不同。两个模型的 PR-AUC 得分都超过了‘无技能’分类器的基线表现(该分类器在所有情况下都预测主要类别),但在精确度和召回率上仍有改进空间:随机森林的 PR-AUC 得分为 0.46,而决策树仅为 0.20。这些结果强调了在不平衡场景下将 PR-AUC 作为目标指标的重要性,并建议了未来改进的方向,如尝试不同的类别不平衡缓解技术、调整决策阈值或探索更适合不平衡数据的算法。

随机森林和决策树模型在松鼠普查数据上的训练和超参数调优结果。图片由作者提供。

特征重要性分析还显示,纬度、经度和 Indifferent 是对模型预测贡献最大的主要特征。这一点通过两种模型的 SHAP 值图进一步得到了支持。

随机森林和决策树模型在松鼠普查数据集上的 SHAP 值。图片由作者提供。

随机森林和决策树模型的概率分布如下所示。直方图显示了预测概率的分布,峰值在 0.1 到 0.2 之间,并有一个长尾延伸至 0.8。这表明随机森林模型倾向于预测许多实例为低概率正类,但存在不确定性,因为它没有在 0 或 1 附近达到峰值。较高概率的尾部存在,表明模型对某些预测有一定的信心,但总体上预测较为谨慎,因为概率并未集中在 0 或 1 附近。决策树直方图显示了在概率 0 处极为集中的分布,以及很少有概率为 1 的实例。这表明模型对大多数实例属于负类非常自信。缺乏中间概率的实例表明决策树模型的预测非常确定——它要么非常确信一个实例是正类,要么非常确信它不是。这是决策树的典型特征,因为它们往往会产生更极端的概率估计。

随机森林和决策树模型在松鼠普查数据上的概率分布。图片由作者提供。

随机森林模型可能会更好地进行校准,因为它提供了更平滑的概率分布,并不像决策树那样趋向极端。决策树的极端自信可能是过拟合的迹象,即模型过于精确地学习了训练数据,包括噪声,从而导致过于自信的预测。两个模型都没有显示出理想的双峰分布,这会表明在区分类别方面的高自信。这可能表明模型没有完美地捕捉到潜在模式,可能需要进一步调优、添加特征或采用更复杂的建模技术。我当然可以多尝试一些特征,但我会将其留到将来的迭代中。因此,我决定对表现更好的随机森林模型进行概率校准是一个好的下一步。

校准模型

校准机器学习模型是调整预测概率以更好地与数据中的观测频率对齐的过程。这是一个在训练模型时常常被忽视的步骤,限制了其可用性。校准模型很重要,因为机器学习模型的原始估计可能无法表示事件的真实可能性。如果你的模型用于任何类型的决策过程(特别是那些涉及高风险的),这一点尤为关键。此外,如果你的概率经过良好校准,这也可以使不同模型之间的比较更具意义。

为了进行校准,我使用了 Platt Scaling(也称为 Sigmoid 校准)。该方法将逻辑回归模型拟合到分类器的输出。基本工作流程如下:

  1. 训练模型

  2. 划分数据

  3. 拟合校准模型

  4. 评估校准

该过程的代码如下所示。

from sklearn.calibration import CalibratedClassifierCV
from sklearn.model_selection import train_test_split
from sklearn.metrics import brier_score_loss
from sklearn.calibration import calibration_curve
import matplotlib.pyplot as plt

# Split your data into training and calibration sets
X_train_2, X_calib, y_train_2, y_calib = train_test_split(X_train, y_train, test_size=0.2)

# Fit the model on the training data
best_rf_classifier.fit(X_train_2, y_train_2)

# Calibrate the model on the calibration data
# Using Platt scaling (method='sigmoid') or Isotonic regression (method='isotonic')
calibrated_rf  = CalibratedClassifierCV(estimator=best_rf_classifier, method='isotonic', cv='prefit')
calibrated_rf.fit(X_calib, y_calib)

# Now you can use calibrated_model to make predictions with calibrated probabilities
calibrated_probs = calibrated_rf.predict_proba(X_test)

# Predict probabilities on the test set
brier_probs = calibrated_rf.predict_proba(X_test)[:, 1]

# Evaluate calibration performance
brier_score = brier_score_loss(y_test, brier_probs)
print(f"Brier score: {brier_score:.4f}")

# Get the calibrated probabilities for the positive class
prob_true, prob_pred = calibration_curve(y_test, calibrated_probs[:, 1], n_bins=20)

校准结果如下所示。一个完全校准的模型应将所有点精确地位于橙色曲线之上。总体而言,概率校准较好,唯一的显著例外是在 ~ 0.25 处,模型明显低估了概率。此外,我还计算了校准过程中的 Brier 分数。Brier 分数范围在 0 和 1 之间,0 代表完美准确,1 代表完全不准确。此次校准的 Brier 分数为 0.0445,比较低,因此表现良好。

使用 Platt scaling 对随机森林模型进行概率校准。图片来源于作者。

尽管我可以对这一过程进行进一步改进,但这将是我在未来的迭代中精细化的内容。目前,我会简单地保存模型并继续。

import joblib

# Save the model to a file
joblib.dump(calibrated_rf, '/content/drive/MyDrive/SquirrelML/cal_Squirrel_RF.pkl')

将模型部署为 Streamlit 应用

尽管有很多机器学习模型被创建,但遗憾的是,很多都被束之高阁。我认为,无论你在进行 ML 项目时都应有一种方式,使你的模型能够被部署并被人们使用。这样,即使只是像我这里做的那种简单应用,你的工作也有机会得到认可,发挥更大的作用。我发现人们通常会更记得你的工作,如果他们能以某种方式与之互动(这对于求职时尤其有用)。将模型部署为 Streamlit 应用是一种很好地展示你工作的方式。

这个应用的基本前提是给用户提供各种功能的操作体验,用于训练模型并输出一个松鼠接近他们的概率。模型的输入之一是用户的经纬度。用户可以直接在应用中输入这些数据,不过,我还使用了 Folium 来包含一个交互式地图,初始时中心定位在中央公园,并且当你点击地图上的位置时,会显示该位置的经纬度。

SquirrelML Streamlit 应用,用于预测中央公园松鼠的接近情况。图像由作者提供。

用户可以在下拉菜单中尝试不同的功能,一旦他们配置好所需的功能,可以点击预测按钮。点击预测按钮将返回松鼠接近概率(SAP)以及一张附带的图片(如果概率超过 50%,则会是一只开心的松鼠;如果概率等于或低于 50%,则会是一只伤心的松鼠图片)。你可以在下面看到一个示例输出:

结论

这是一个有趣的小项目。模型当然可以改进,我可能会在未来重新审视以进一步优化。例如,我可以尝试将我的 PCA 分析结果与 SHAP 值结合使用,尝试将数据集的维度降至核心特征。还有一些与松鼠普查相关的数据集,包含了如垃圾量、活动水平、观察时长和天气条件等信息。我在处理完数据后才发现这些数据集,但我很希望将它们整合到我的模型中,因为它们似乎很有用。这些数据集可以在 NYCOpenData 的这里这里找到。我还可以尝试使用数据增强技术。

一如既往,希望你喜欢我的工作,谢谢阅读!

来自 NYCOpenData 的所有数据在使用上没有限制。请查看以下链接了解其常见问题使用条款

Stable Diffusion 作为 API:创建一个去除人物的微服务

原文:towardsdatascience.com/stable-diffusion-as-an-api-5e381aec1f6

使用 Stable Diffusion 2 生成的风景图像(作者提供)。

使用 Stable Diffusion 微服务从照片中去除人物

Mason McGoughTowards Data Science Mason McGough

·发表在 Towards Data Science ·阅读时间 12 分钟·2023 年 2 月 4 日

--

概述

Stable Diffusion 是一个前沿的开源工具,用于从文本生成图像。Stable Diffusion Web UI 通过 API 以及交互式用户界面提供了许多这些功能。我们将首先介绍如何使用这个 API,然后设置一个示例,将其作为一个隐私保护微服务来从图像中去除人物。

生成 AI 介绍

去年,基于机器学习的数据生成器出现了许多创新,2022 年或许可以称为“生成 AI 的年度”。我们迎来了 DALL-E 2,这是 OpenAI 的文本到图像生成模型,生成了令人惊叹的现实主义图像,如宇航员骑马和穿着人类衣物的狗。 GitHub Copilot,这个强大的代码补全工具可以自动完成语句、编写文档,并根据单个评论实现完整的功能,已经作为订阅服务公开发布。我们还看到了 Dream FieldsDream FusionMagic3D,一系列能够仅通过文本生成纹理化 3D 模型的突破性模型。最后但同样重要的是我们迎来了 ChatGPT,这个前沿的 AI 聊天机器人如今无需介绍。

这个列表几乎只是触及了表面。在像 DALL-E 2 这样的生成图像模型世界中,我们还有MidjourneyGoogle ImagenStarryAIWOMBO DreamNightCafeInvokeAILexica ApertureDream StudioDeforum……我想你已经明白了画面。😉 📷 说生成 AI 已经捕捉了整个世界的想象力,似乎并不夸张。

稳定扩散

尽管许多流行的生成 AI 工具如 ChatGPT、GitHub Copilot 和 DALL-E 2 是专有的且需付费的,但开源社区并没有停下脚步。去年,LMU 慕尼黑大学、Runway 和 Stability AI 合作公开分享了Stable Diffusion,这是一个强大但高效的文本到图像模型,足够高效以在消费级硬件上运行。这意味着任何拥有一台不错 GPU 和互联网连接的人都可以下载 Stable Diffusion 代码和模型权重,将低成本图像生成带给世界。

稳定扩散网络界面

Stable Diffusion Web UI是利用 Stable Diffusion 的最受欢迎工具之一,它在基于浏览器的用户界面中暴露了 Stable Diffusion 的广泛设置和功能。该项目一个鲜为人知的功能是,你可以将其作为 HTTP API 使用,允许你从自己的应用程序中请求图像。

Stable Diffusion Web UI 的示例生成(作者提供的照片)。

它具有大量功能,如修补、扩展、调整大小、放大、变体等。 项目 wiki 提供了所有功能的详细概述。此外,它还提供了扩展性脚本。

设置

在开始之前,请确保你的系统中有一个 GPU(最好是 NVIDIA,但 AMD 也支持),且至少有 8GB 的显存。这将确保你可以将模型加载到内存中。接下来,你需要将代码库克隆到你的系统中(例如通过 HTTPS):

git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git

根据你的系统遵循安装说明,因为这些可能与你的有所不同。我使用的是 Ubuntu 18.04 进行设置,但它也应该适用于 Windows 和 Apple Silicon。这些说明将包括设置 Python 环境,因此请确保在稍后启动服务器时,所设置的环境处于活动状态。

完成后,我们需要一份模型权重的副本。我使用的是 Stable Diffusion 2.0,但现在也有 Stable Diffusion 2.1 可用。无论你选择哪个选项,确保下载 stablediffusion 仓库的权重。最后,将这些权重复制到 models/Stable-diffusion 文件夹中,如下所示:

cp 768-v-ema.ckpt models/Stable-diffusion

现在你应该准备好开始生成图像了!要启动服务器,请在根目录下执行以下操作(确保你设置的环境已激活):

python launch.py

服务器需要一些时间来设置,因为它可能需要安装要求、将模型权重加载到内存中、检查嵌入等。准备好后,你应该会在终端中看到类似下面的消息:

Running on local URL:  http://127.0.0.1:7860

这个用户界面是基于浏览器的,因此在你喜欢的网页浏览器中导航到 “127.0.0.1:7860” 。如果它正常工作,它应该看起来像这样:

稳定扩散 Web 用户界面首次打开时(作者拍摄)。

使用

现在你应该准备好生成一些图像了!进入“提示”字段中输入文本并点击“生成”以生成一些内容。如果这是你第一次使用这个用户界面,花点时间探索并了解其一些功能和设置。如果有任何问题,请参考 维基。这些知识在设计你的 API 时会很有用。

我不会过于深入地讲解如何使用网页用户界面,因为之前有很多人已经做了。不过,我会提供以下基本设置的备忘单以供参考。

  • 采样方法:采样算法。这会极大地影响生成图像的内容和整体外观。不同方法的执行时间和结果可能会有很大差异。理想情况下,首先尝试这个选项。

  • 采样步骤:图像生成过程中的去噪步骤数。某些结果会随着步骤数量的变化而剧烈改变,而其他结果则会很快出现边际效益递减。大多数采样器理想的步骤数为 20–50。

  • 宽度高度:输出图像的尺寸。对于 SD 2.0,768x768 是首选分辨率。分辨率会影响生成的内容。

  • CFG 比例无分类器引导 (CFG) 比例。增加这个比例会增加图像受到提示的影响。较低的值会产生更具创意的结果。

  • 去噪强度:确定允许原始图像的变化程度。值为 0.0 时没有变化。值为 1.0 时完全忽略原始图像。一般来说,从 0.4–0.6 的值开始是一个安全的选择。

  • 种子:随机种子值。当你想比较设置效果而尽可能减少变化时,这很有用。如果你喜欢某个特定生成但想稍作修改,请复制种子。

将 Stable Diffusion 作为 API 使用

网络用户界面旨在供单个用户使用,并且作为一个互动艺术工具制作自己的创作非常好。然而,如果我们想要使用它作为引擎来构建应用程序,我们将需要一个 API。stable-diffusion-webui 项目的一个鲜为人知(且文档较少)的功能是它还具有内置的 API。网络用户界面是用 Gradio 构建的,但也有一个可以用以下命令启动的 FastAPI 应用:

python launch.py --api

这为我们提供了一个 API,暴露了我们在网络用户界面中拥有的许多功能。我们可以发送带有提示和参数的 POST 请求,并接收包含输出图像的响应。

创建一个微服务

作为示例,我们现在将设置一个简单的微服务,用于从照片中移除人物。这有很多应用,例如保护个人隐私。我们可以使用稳定扩散作为一种原始的隐私保护过滤器,它可以从照片中去除人物而没有任何难看的马赛克或像素块。

请注意,这只是一个基本设置;它不包括加密、负载均衡、多租户、RBAC 或任何其他功能。这个设置可能不适合生产环境,但可以用于在家庭或私人服务器上设置应用程序。

以 API 模式启动应用程序

以下说明将使用 API 模式运行服务器,因此请先通过 CTRL+C 停止网络用户界面(web UI)。然后使用 --api 选项以 API 模式重新启动它:

python launch.py --api

当服务器准备好时,它应该会打印出类似以下内容的信息:

INFO:     Uvicorn running on http://127.0.0.1:7860 (Press CTRL+C to quit)

运行服务器而不带用户界面的无头状态也可能很有用。要仅启用 API 而不使用 Gradio 应用:

python launch.py --nowebui

向 API 发送请求

我们首先要做的是演示如何向 API 发出请求。我们希望向应用程序的 txt2img(即“文本到图像”)API 发送 POST 请求,以简单地生成一张图像。

我们将使用 requests 包,因此如果你还没有安装,请先安装它:

pip install requests

我们可以发送一个包含简单字符串的提示的请求。服务器将返回一个 base64 编码的 PNG 文件,我们需要对其进行解码。要解码 base64 图像,我们只需使用 base64.b64decode(b64_image)。以下脚本应该可以测试这个功能:

import json
import base64

import requests

def submit_post(url: str, data: dict):
    """
    Submit a POST request to the given URL with the given data.
    """
    return requests.post(url, data=json.dumps(data))

def save_encoded_image(b64_image: str, output_path: str):
    """
    Save the given image to the given output path.
    """
    with open(output_path, "wb") as image_file:
        image_file.write(base64.b64decode(b64_image))

if __name__ == '__main__':
    txt2img_url = 'http://127.0.0.1:7860/sdapi/v1/txt2img'
    data = {'prompt': 'a dog wearing a hat'}
    response = submit_post(txt2img_url, data)
    save_encoded_image(response.json()['images'][0], 'dog.png')

将内容复制到一个文件中,并将其命名为 sample-request.py。现在使用以下命令执行它:

python sample-request.py

如果一切正常,它应该会将图像保存为文件 dog.png。我的图像看起来像这样这位衣着光鲜的家伙:

使用‘sample-request.py’创建的图像(作者拍摄的照片)。

请记住,您的结果可能与我的不同。如果遇到问题,请仔细检查运行稳定扩散应用的终端输出。可能是服务器尚未完成设置。如果遇到“404 Not Found”的问题,请仔细检查 URL 是否正确输入,并指向正确的地址(例如 127.0.0.1)。

图像掩膜。

如果到目前为止一切正常,那就太好了!但是我们如何使用它来修改已有的图像呢?为此,我们将使用 img2img(即“图像到图像”)API。该 API 使用稳定扩散来修改您提交的图像。我们将使用 修复功能:给定图像和掩膜,修复技术将尝试用稳定扩散生成的内容替换图像中被掩膜遮挡的部分。掩膜作为权重,平滑地插值于原始图像和生成内容之间,将两者融合在一起。

我们将尝试使用许多可用的预训练计算机视觉模型之一生成掩膜,而不是手动制作掩膜。我们将使用模型输出中的“person”类别来生成掩膜。虽然对象检测模型也可以,但我选择使用分割模型,以便您可以尝试使用密集掩膜或边界框。

我们需要一个示例图像进行测试。我们可以从互联网上下载一个,但为了保护隐私(和版权),为什么不使用稳定扩散生成一个呢?以下是我使用提示词“美丽的山地风景,一个女人背对镜头走开”生成的图像。

稳定扩散生成的图像(照片作者提供)。

您可以下载这个,但我鼓励您尝试自己生成一个。当然,您也可以使用真实照片。以下是将 torchvision 的库存分割模型应用于此图像作为掩膜的最小代码。

import torch
from torchvision.models.segmentation import fcn_resnet50, FCN_ResNet50_Weights
from torchvision.io.image import read_image
from torchvision.utils import draw_segmentation_masks
import matplotlib.pyplot as plt

if __name__ == '__main__':
    img_path = 'woman-on-trail.png'

    # Load model
    weights = FCN_ResNet50_Weights.DEFAULT
    model = fcn_resnet50(weights=weights, progress=False)
    model = model.eval()

    # Load image
    img = read_image(img_path)

    # Run model
    input_tform = weights.transforms(resize_size=None)
    batch = torch.stack([input_tform(img)])
    output = model(batch)['out']

    # Apply softmax to outputs
    sem_class_to_idx = {cls: idx for (idx, cls) in enumerate(weights.meta['categories'])}
    normalized_mask = torch.nn.functional.softmax(output, dim=1)

    # Show results
    class_idx = 1
    binary_masks = (normalized_mask.argmax(class_idx) == sem_class_to_idx['person'])
    img_masked = draw_segmentation_masks(img, masks=binary_masks, alpha=0.7)
    plt.imshow(img_masked.permute(1, 2, 0).numpy())
    plt.show()

像之前一样,将其复制到名为 segment-person.py 的文件中。使用以下命令执行代码:

python segment-person.py

结果预测应该类似于这个:

应用到图像上的分割掩膜结果(照片作者提供)。

现在我们有了向 API 发出请求和预测边界框的工具。现在可以开始构建我们的微服务了。

人物移除微服务。

现在让我们转到实际示例:从图像中移除人物。微服务应执行以下操作:

  1. 读取一些输入参数。

  2. 从文件中加载图像。

  3. 将“person”类别的分割模型应用于图像以创建掩膜。

  4. 将图像和掩膜转换为 base64 编码。

  5. 发送一个请求,包含 base64 编码的图像、base64 编码的掩膜、提示词和任何参数到本地服务器的 img2img API。

  6. 解码并将输出图像保存为文件。

由于我们已经单独涵盖了所有这些步骤,微服务已经为您在 这个 GitHub Gist 中实现了。现在下载脚本并使用以下命令在“woman-on-trail.png”(或您喜欢的任何图片)上执行:

python inpaint-person.py woman-on-trail.png -W 1152 -H 768

-W-H 分别表示所需的输出宽度和高度。它将生成的图像保存为 inpaint-person.png,对应的掩模保存为 mask_inpaint-person.png。您的输出会有所不同,但这是我收到的结果:

使用原始分割掩模的 API 调用结果(图像由作者提供)。

嗯,这不是我们想要的。似乎很多人的轮廓仍然存在,特别是剪影。我们可能需要掩盖更大的区域。为此,我们可以尝试将掩模转换为边界框。我们可以使用-B标志来完成这一操作。

python inpaint-person.py woman-on-trail.png -W 1152 -H 768 -B

我收到的输出是:

使用边界框作为掩模的 API 调用结果(照片由作者提供)。

这也不完全正确!混凝土柱子不是我们期望在小径中间找到的东西。也许引入一个提示会有助于引导方向。我们使用-p标志将提示“山地风景,景观,小径”添加到请求中。我们还使用-D 32扩展边界框,以去除一些边缘效果,并使用-b 16模糊边界框,使掩模与背景稍微融合。

python inpaint-person.py woman-on-trail.png \
    -W 1152 -H 768 \
    -b 16 -B -D 32 \
    -p "mountain scenery, landscape, trail"

使用这个我得到了以下输出:

最终 API 调用的结果(照片由作者提供)。

现在看起来很有可能!继续尝试不同的图片、设置和提示,以便适应您的用例。要查看该脚本可用的完整参数和提示列表,请输入 python inpaint-person.py -h

讨论

您的图片很可能与上述图片差别很大。因为这是一个本质上随机的过程,即使使用相同设置的稳定扩散也会产生截然不同的结果。理解所有特性和正确的提示设计有相当陡峭的学习曲线,即便如此,结果也可能很挑剔。让一张图片完全符合您的喜好极其困难,需要经过大量的试验和错误。

为了帮助您,请记住以下提示:

  • 在转到 API 之前,使用 Web UI 找到适合您用例的正确参数。

  • 在将图像微调到您喜欢的状态时,依赖于提示矩阵和 X/Y 图特性。这些将帮助您快速探索参数搜索空间。

  • 请注意种子。如果您喜欢特定的输出但想要对其进行迭代,请复制种子。

  • 尝试使用不同的生成器,如 Midjourney!每个工具略有不同。

  • 使用互联网资源,如 Lexica 来获取灵感并寻找好的提示。

  • 在设置菜单中使用“在每张图片旁边创建一个文本文件,并记录生成参数”选项,以跟踪你用来制作每张图片的提示和设置。

最重要的是,要玩得开心!

稳定扩散:掌握室内设计的艺术

原文:towardsdatascience.com/stable-diffusion-mastering-the-art-of-interior-design-9fb4214544b0

深入探索稳定扩散及其室内设计的修复变体

Rafael GuedesTowards Data Science Rafael Guedes

·发布于Towards Data Science ·阅读时间 9 分钟·2023 年 12 月 18 日

--

在我们生活的这个快节奏的世界中以及经历了疫情后,我们中的许多人意识到,拥有一个舒适的家作为逃避现实的避风港是无价的,值得追求的目标。

无论你是寻找斯堪的纳维亚风格、极简风格,还是华丽风格来装饰你的家,很难想象每件物品如何在充满不同件数和颜色的空间中相互配合。因此,我们通常寻求专业帮助,以创建那些令人惊叹的 3D 图像,帮助我们理解未来的家会是什么样子。

然而,这些 3D 图像费用昂贵,如果我们的初步构思不如预期好,获取新图像将需要时间和更多的金钱,这些在当今社会都很稀缺。

在这篇文章中,我探讨了稳定扩散模型,首先简要解释了它是什么、如何训练以及需要什么来适应修复。最后,我以对我未来家的 3D 图像的应用结束文章,我将厨房岛和橱柜的颜色和材料更换为不同的。

图 1:室内设计 (source)

一如既往,代码可在Github上找到。

稳定扩散

这是什么?

稳定扩散[1]是 CompVis Group 于 2022 年发布的生成性 AI 模型,可以根据文本和图像提示生成逼真的图像。它主要设计用于生成受文本描述影响的图像,但也可以用于其他任务,如修复或视频创建。

它的成功源于 感知图像压缩 步骤,该步骤将高维图像转换为更小的潜在空间。这种压缩使得在资源有限的机器上使用该模型成为可能,使每个人都能使用,这在之前的最先进模型中是不可能的。

图 2:Stable Diffusion 架构 (source)

它是如何学习的?

Stable Diffusion 是一个 潜在扩散模型 (LDM),具有三个主要组件 (变分自编码器 (VAE) [2]、U-Net [3] 和一个可选的 文本编码器),它学习如何在提示(文本或其他图像)的条件下去噪图像,以创建新图像。

Stable Diffusion 的训练过程有 5 个主要步骤:

  1. 感知图像压缩 步骤包括一个 编码器,它接收一个尺寸为 512x512x3 的图像,并将其编码为一个尺寸为 64x64x4 的更小的潜在空间 Z。为了更好地保留图像的细节(例如,人脸中的眼睛),潜在空间 Z 使用低权重的 Kullback-Leibler 项进行正则化,以使其零中心并获得小方差。

图 3:感知图像压缩过程,其中编码器将 512x512x3 的图像转换为 64x64x4 的潜在空间(图像由作者制作)。

2. 扩散过程 负责逐步向潜在空间 Z 添加高斯噪声,直到所有剩下的是随机噪声,生成一个新的潜在空间 Zt. t 是扩散过程发生的次数,以实现完全的噪声潜在空间。

这一步骤很重要,因为 Stable Diffusion 必须学习如何从噪声恢复到原始图像,正如我们将在下一步中看到的那样。

图 4:扩散过程,其中高斯噪声逐渐添加到潜在空间中(图像由作者制作)

3. 去噪 过程 训练了一个 U-Net 架构来估计潜在空间 Zt 中的噪声量,以便减去它并恢复 Z。这个过程通过逐渐去噪 Zt 来恢复原始的潜在空间 Z,基本上是扩散过程的逆过程。

图 5:去噪过程,其中 U-Net 预测潜在空间中的噪声并将其去除,直到完全恢复原始潜在空间(图像由作者制作)

4. 在 去噪 过程 中,可以将提示,通常是文本和/或其他图像,连接到潜在空间 Zt。这种连接将条件化去噪过程,从而允许创建新图像。作者在 U-Net 的骨干网络中添加了交叉注意机制来处理这些提示,因为它们对学习各种输入类型的基于注意的模型是有效的。

当涉及到文本时,模型使用训练好的文本编码器CLIP [4],该编码器将提示编码为一个 768 维的向量,然后将其与Zt连接,并作为输入传递给 U-Net。

正如我们在图 6 中所见,我们将文本提示“移除灯”Zt进行连接,这使得 Diffusion Process 对Zt进行调整,去除了原始Zt中靠近椅子的灯。

图 6:使用文本提示来去除原始图像中的灯光的去噪过程(图片由作者制作)

5. 最终,解码器接收去噪后的潜在空间Z作为输入,并学习如何估计用于将图像编码到更小潜在空间中的每个组件的方差。在估计方差后,解码器可以生成与原始图像相同尺寸的新图像。

图 7:解码器恢复了没有灯的原始图像,尺寸为 512x512x3(图片由作者制作)

Stable Diffusion 的修补变体

修补任务是用新内容填补图像中被遮蔽的区域,无论是因为我们想要修复图像还是因为我们想要替换一些不希望出现的内容。

Stable Diffusion 可以被训练生成基于图像、文本提示和掩膜的新图像。这种模型已经在 HuggingFace 🤗 上可用,名称为runwayml/stable-diffusion-inpainting

为了训练 Stable Diffusion 进行修补,我们需要经历上述部分提到的相同步骤,但输入数据稍有不同。在这种情况下,除了原始图像和文本,我们还需要一个掩膜(另一张图像)。为此,U-Net 需要适应以接收额外的掩膜输入通道。

在训练过程中,掩膜下的区域保持不变,只对其进行编码到潜在空间,而掩膜区域则经历整个编码、扩散和去噪过程。这样,Stable Diffusion 模型知道哪些区域应该保持不变,哪些区域应该发生变化(图 8 展示了这一过程)。

图 8:修补扩散 Stable 模型的训练过程(图片由作者制作)

在图 9 中,我们展示了在自己的使用案例中进行修补所需的示例。我们提供原始图像以及我们希望更改的掩膜和包含我们想看到更改的文本提示,Stable Diffusion 生成新图像。在下一节中,我们将看到如何实际操作。

图 9:训练 Stable Diffusion 进行图像修补的过程(图片由作者制作)

使用 Stable Diffusion 的室内设计

在本节中,我将讲解如何在室内设计中使用 Stable Diffusion 进行修补场景的操作。

当我们购买一栋新房或仍在建设中的公寓时,通常可以获取它的 3D 图像。基于这些图像,我们可以要求更改颜色或材料,以使其符合我们的口味。

然而,很难想象我们请求的更改是否适合房子的其他部分,而请求新的 3D 可能会很昂贵且耗时。因此,我们可以使用 Stable Diffusion 快速迭代,并了解如果应用我们想要的更改,事物会是什么样子。

为此,我们可以使用 Python 和 HuggingFace 🤗 来构建我们自己的 Stable Diffusion 室内设计师!

我们首先导入库:

import PIL
import torch
import ipyplot
from diffusers import StableDiffusionInpaintPipeline

然后,我们加载 HuggingFace 🤗 提供的 Stable Diffusion Inpainting 模型:

# load model
pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16).to("cuda")

在加载了模型后,我们加载原始图像和我们想要更改的遮罩:

  • 遮罩的白色部分是将要更改的部分,而黑色部分是将保持不变的部分。

  • 遮罩是手动创建的,但我们也可以使用分割模型来创建它。

init_image = PIL.Image.open("<YOUR IMAGE>.jpg")
mask_image = PIL.Image.open("<YOUR MASK>.png")

ipyplot.plot_images([init_image, mask_image], labels=["original", "mask"], max_images=30, img_width=500)

图 10:厨房的 3D 图像(由作者购买)和一个遮罩,用于更改岛台和黑色墙壁(图像由作者制作)

在加载了图像和遮罩后,现在是创建提示以将图像生成条件设置为我们想要的内容并生成新图像的时候了。

  • 在这种情况下,我想将黑色岛台和黑色墙壁替换为大理石岛台和大理石墙壁。
# define prompt
prompt = "white calacatta marble island kitchen and white calacatta marble wall"
guidance_scale = 15 # weight of the prompt
generator = torch.Generator(device="cuda").manual_seed(0) # change the seed to get different results

# generate 10 images
images = pipe(prompt=prompt, image=init_image, mask_image=mask_image, num_images_per_prompt=10, guidance_scale=guidance_scale, generator=generator).images

# show results
print(prompt)
ipyplot.plot_images([init_image] + images, labels=["original"] + ["generated"] * len(images), max_images=30, img_width=500)

图 11:原始图像与我最喜欢的生成图像的比较(它们的尺寸不同,因为模型是用 512x512 图像训练的)

结果看起来不错,但我还想将木制厨房橱柜替换为白色的,所以让我们重新进行这个过程:

  1. 加载最后生成的图像和一个新的遮罩:
# load new image and mask
init_image = PIL.Image.open("IMG2.jpg") mask_image = PIL.Image.open("mask3.png")
ipyplot.plot_images([init_image, mask_image], labels=["original", "mask"], max_images=30, img_width=500)

图 12:之前生成的图像和新的遮罩(图像由作者制作)

  1. 创建新的提示并生成新图像:
# define prompt
prompt = "white kitchen cabinet"
guidance_scale = 15 # weight of the prompt
generator = torch.Generator(device="cuda").manual_seed(0) # change the seed to get different results

# generate 10 images
images = pipe(prompt=prompt, image=init_image, mask_image=mask_image, num_images_per_prompt=10, guidance_scale=guidance_scale, generator=generator).images

# show results
print(prompt)
ipyplot.plot_images([init_image] + images, labels=["original"] + ["generated"] * len(images), max_images=30, img_width=500)

图 13:带有白色厨房橱柜的生成图像(图像由作者制作)

结果看起来非常好,我希望你注意到 Stable Diffusion 能够重现的细节,例如厨房水龙头或橱柜上的光线反射。尽管左侧的反射没有对齐,但令人惊讶的是它如何考虑了光线。

结论

AI 不仅对拥有大量数据的组织有用,它还可以应用于我们能想到的任何事物!

在本文中,我们探索了 Stable Diffusion 的一种非传统用途,但这是一个存在了几十年的传统工作。通过几行代码,我们能够为每个提示生成尽可能多的不同图像,这给了我们很多选择的可能性。

然而,与其他事物一样,稳定扩散模型,特别是我们使用的模型,也有其自身的局限性,例如在光线反射中或在图 14 中,其中一把椅子位于厨房岛内,并未实现完美的真实感。

尽管如此,人工智能的未来仍然光明,其中一些局限性可以通过在我们自己的数据和使用案例中进行微调来克服。

图 14:生成的图像存在一些质量问题(图像由作者制作)

保持联系: LinkedInMedium

参考文献

[1] Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser, Björn Ommer. 高分辨率图像合成与潜在扩散模型。arXiv:2001.08210, 2022

[2] Diederik P. Kingma, Max Welling. 变分自编码器简介。arXiv:1906.02691, 2019

[3] Olaf Ronneberger, Philipp Fischer, Thomas Brox. U-Net: 用于生物医学图像分割的卷积网络。arXiv:1505.04597, 2015

[4] Alec Radford, Jong Wook Kim, Chris Hallacy, Aditya Ramesh, Gabriel Goh, Sandhini Agarwal, Girish Sastry, Amanda Askell, Pamela Mishkin, Jack Clark, Gretchen Krueger, Ilya Sutskever. 从自然语言监督中学习可转移的视觉模型。arXiv:1911.02116, 2021

我如何通过堆叠集成模型在欧洲最大机器学习竞赛中获得前 10%

原文:towardsdatascience.com/stacked-ensembles-for-advanced-predictive-modeling-with-h2o-ai-and-optuna-8c339f8fb602

关于使用 H2O.ai 和 Optuna 训练堆叠集成模型的概念性和实践编码指南

Sheila TeoTowards Data Science Sheila Teo

·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 12 月 18 日

--

图像由 DALL·E 3 生成

我们都知道,集成模型在预测建模中优于任何单一模型。你可能听说过 Bagging 和 Boosting 作为常见的集成方法,以随机森林和梯度提升机作为各自的例子。

但是,如何在一个独立的更高层次模型下组合不同的模型呢?这就是堆叠集成模型的作用所在。本文是如何使用流行的机器学习库 H2O 训练堆叠集成模型的逐步指南。

为了展示堆叠集成模型的强大功能,我将提供一个完整的代码演示,介绍如何训练一个由 40 个深度神经网络、XGBoost 和 LightGBM 模型组成的堆叠集成模型,以完成 2023 年 Cloudflight 编程竞赛(AI 类别)中的预测任务。这是欧洲最大的编程竞赛之一,我在其中的训练时间为 1 小时内获得了前 10% 的名次!

本指南将涵盖:

  1. 什么是堆叠集成模型,它们是如何工作的?

  2. 如何使用 H2O.ai 训练堆叠集成模型 **—

    通过 Python 的完整代码演示**

  3. 比较堆叠集成模型与单独模型的性能

1. 什么是堆叠集成模型,它们是如何工作的?

堆叠集成模型通过另一个更高层次的模型将多个模型的预测结果进行结合,旨在通过利用每个组成模型的独特优势来提高整体预测性能。它包括两个阶段:

阶段 1:多个基础模型

首先,多个基础模型在相同的训练数据集上独立训练。这些模型应当尽可能多样化,从简单的线性回归到复杂的深度学习模型都可以。关键是它们在某些方面应有所不同,无论是使用不同的算法还是使用相同的算法但具有不同的超参数设置。

基础模型的多样性越高,最终的堆叠集成模型越强大。 这是因为不同的模型能够捕捉数据中的不同模式。例如,基于树的模型可能擅长捕捉非线性关系,而线性模型擅长理解线性趋势。当这些多样化的基础模型结合在一起时,堆叠集成模型可以利用每个基础模型的不同优势,从而提高预测性能。

第二阶段:一个元模型

在所有基础模型训练完成后,每个基础模型对目标的预测将用作训练一个更高层次模型(称为元模型)的特征。这意味着元模型不是在原始数据集的特征上训练,而是在基础模型的预测上进行训练。如果有n个基础模型,则生成n个预测,这些预测就是用于训练元模型的n个特征。

虽然训练特征在基础模型和元模型之间有所不同,但目标保持不变,即数据集中的原始目标。

元模型学习如何最佳地结合基础模型的预测,以做出最终的、更准确的预测。

堆叠集成训练的详细步骤

对于每个基础模型:

1. 选择一个算法(例如,随机森林)。

2. 使用交叉验证获得算法的最佳超参数集合。

3. 获取训练集中目标的交叉验证预测。这些将随后用于训练元模型。

为了说明这一点,假设在步骤 1 中选择了随机森林算法,并且在步骤 2 中确定了其最优超参数为 *h*

*交叉验证预测是通过以下方式获得的,假设使用 5 折交叉验证:

1. 在第 1 到第 4 折上训练具有超参数* *h* *的随机森林。

2. 使用训练好的随机森林对第 5 折进行预测。这些是第 5 折的交叉验证预测。

3. 重复上述步骤以获取每一折的交叉验证预测。之后,将获得整个训练集的目标交叉验证预测。*

对于元模型:

1. 获取用于训练元模型的特征。这些是每个基础模型的预测。

2. 获取用于训练元模型的目标。这是来自训练集的原始目标。

3. 选择一个算法(例如,线性回归)。

4. 使用交叉验证获得算法的最佳超参数集合。

看,完成了!你现在拥有:

  • 多个经过优化超参数训练的基础模型

  • 一个也经过优化超参数训练的元模型

这意味着你已经成功训练了一个堆叠集成模型!

2. 如何使用 H2O.ai 训练堆叠集成模型

现在,让我们开始编写代码吧!

如前所述,本节包括我为 2023 Cloudflight 编码竞赛(AI 类别)中的预测任务训练堆叠集成模型的完整代码,这是一个使用表格数据的回归任务。在竞赛时间限制下,我从 40 个基础模型中创建了一个堆叠集成模型,涉及 3 种算法类型——深度神经网络、XGBoost 和 LightGBM,这些特定算法被选择是因为它们在实践中通常表现优异。

2.1. 数据准备

首先,让我们导入必要的库。

import pandas as pd
import h2o
from h2o.estimators.deeplearning import H2ODeepLearningEstimator
from h2o.estimators import H2OXGBoostEstimator
from h2o.estimators.stackedensemble import H2OStackedEnsembleEstimator
import optuna
from tqdm import tqdm

seed = 1

并初始化 H2O 集群。

h2o.init()

接下来,加载数据集。

data = pd.read_csv('path_to_your_tabular_dataset')

在使用 H2O 进行模型构建之前,让我们首先了解 H2O 模型的以下特性:

  1. H2O 模型不能接收 Pandas DataFrame 对象,因此 data 必须从 Pandas DataFrame 转换为 H2O 对应的 H2OFrame。

  2. H2O 模型可以自动编码分类特征,这很好,因为这一步预处理被省略了。为了确保这些特征被模型识别为分类特征,它们必须显式转换为因子(分类)数据类型。

data_h2o = h2o.H2OFrame(data)

categorical_cols = [...]  #insert the names of the categorical features here
for col in categorical_cols:
  data_h2o[col] = data_h2o[col].asfactor()

现在我们可以使用 H2OFrame 对象的 split_frame() 方法将数据集拆分为训练集(90%)和验证集(10%)。

splits = data_h2o.split_frame(ratios=[0.9], seed=seed)
train = splits[0]
val = splits[1]

最后,让我们获取建模的特征和目标。与接受特征和目标的的 Scikit-Learn 模型不同,H2O 模型接受的是特征和目标的名称

y = '...'  #insert name of the target column here
x = list(train.columns)
x.remove(y) 

现在,让模型训练的乐趣开始吧!

2.2. 训练深度神经网络(DNN)作为基础模型

我们先从训练将组成堆叠集成模型的 DNN 开始,使用 H2O 的 H2ODeepLearningEstimator

附注:为什么在 H2O 中训练 DNN,而不是使用 Tensorflow、Keras 或 PyTorch?

在跳入代码之前,你可能会好奇为什么我选择使用 H2O 的 *H2ODeepLearningEstimator** 来训练 DNN,而不是使用 Tensorflow、Keras 或 PyTorch 这些常用的 DNN 构建库。*

简单的答案是,在 H2O 中构建堆叠集成模型使用的是 *H2OStackedEnsembleEstimator*,它只能接受 H2O 模型家族中的基础模型。然而,更关键的原因是 H2O 的 *H2ODeepLearningEstimator* 比其他框架更容易调整 DNN,这就是原因。

在 TensorFlow、Keras 或 PyTorch 中,像 dropout 层这样的正则化效果必须手动添加到模型架构中,例如使用 *keras.layers.Dropout()*。这允许更大的自定义,但也需要更多的详细知识和精力。例如,你必须决定在模型架构中在哪里以及多少次包括 *keras.layers.Dropout()* 层。

另一方面,H2O 的 *H2ODeepLearningEstimator* 更加抽象,易于普通人使用。可以通过模型超参数以简单的方式启用正则化,从而减少手动设置这些组件为层的需求。此外, 默认 的模型超参数已包含正则化。常见的特征预处理步骤,例如数值特征的缩放和分类特征的编码,也作为模型超参数包含在内,以实现自动特征预处理。这些功能使得调整深度神经网络(DNN)的过程变得更加直接和简单,而无需深入了解深度学习模型架构的复杂性。在比赛的时间紧迫背景下,这对我来说非常有用!

那么,我们应该使用哪一组超参数来训练H2ODeepLearningEstimator呢?这就是optuna的作用所在。Optuna是一个超参数优化框架,类似于传统的网格搜索和随机搜索方法,但它采用了更复杂的方法。

网格搜索系统地探索预定义范围内的超参数值,而随机搜索则在这些指定的限制范围内选择随机组合。然而,optuna利用贝叶斯优化,从之前的搜索中学习,提出每次后续搜索中表现更好的超参数集,提高了寻找最佳模型超参数的效率。这在复杂且大型的超参数空间中尤其有效,在这些空间中,传统的搜索方法可能会耗时极长,最终仍可能无法找到最佳的超参数集。

现在,让我们进入代码部分。我们将使用optuna来调整 H2O 的H2ODeepLearningEstimator的超参数,并将所有训练过的模型跟踪到列表dnn_models中。

dnn_models = []

def objective(trial):
    #params to tune
    num_hidden_layers = trial.suggest_int('num_hidden_layers', 1, 10)
    hidden_layer_size = trial.suggest_int('hidden_layer_size', 100, 300, step=50)

    params = {
        'hidden': [hidden_layer_size]*num_hidden_layers,
        'epochs': trial.suggest_int('epochs', 5, 100),
        'input_dropout_ratio': trial.suggest_float('input_dropout_ratio', 0.1, 0.3),  #dropout for input layer
        'l1': trial.suggest_float('l1', 1e-5, 1e-1, log=True),  #l1 regularization
        'l2': trial.suggest_float('l2', 1e-5, 1e-1, log=True),  #l2 regularization
        'activation': trial.suggest_categorical('activation', ['rectifier', 'rectifierwithdropout', 'tanh', 'tanh_with_dropout', 'maxout', 'maxout_with_dropout'])
}

    #param 'hidden_dropout_ratios' is applicable only if the activation type is rectifier_with_dropout, tanh_with_dropout, or maxout_with_dropout
    if params['activation'] in ['rectifierwithdropout', 'tanh_with_dropout', 'maxout_with_dropout']:
        hidden_dropout_ratio = trial.suggest_float('hidden_dropout_ratio', 0.1, 1.0)  
        params['hidden_dropout_ratios'] = [hidden_dropout_ratio]*num_hidden_layers  #dropout for hidden layers

    #train model
    model = H2ODeepLearningEstimator(**params,
                                     standardize=True,  #h2o models can do this feature preprocessing automatically
                                     categorical_encoding='auto',  #h2o models can do this feature preprocessing automatically
                                     nfolds=5,
                                     keep_cross_validation_predictions=True,  #need this for training the meta-model later
                                     seed=seed)
    model.train(x=x, y=y, training_frame=train)

    #store model
    dnn_models.append(model)

    #get cross-validation rmse 
    cv_metrics_df = model.cross_validation_metrics_summary().as_data_frame()
    cv_rmse_index = cv_metrics_df[cv_metrics_df[''] == 'rmse'].index
    cv_rmse = cv_metrics_df['mean'].iloc[cv_rmse_index]
    return cv_rmse

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=20)

上述optuna研究用于寻找能够最小化交叉验证 RMSE 的H2ODeepLearningEstimator超参数集(因为这是一个回归任务),优化过程运行 20 次试验,使用参数n_trials=20。这意味着训练了 20 个 DNN,并将它们存储在列表dnn_models中,以供稍后作为堆叠集成的基模型使用,每个模型都有不同的超参数集。考虑到比赛的时间限制,我选择训练 20 个 DNN,但你可以根据自己的需求设置n_trials为你希望训练的 DNN 数量。

重要的是,H2ODeepLearningEstimator必须使用keep_cross_validation_predictions=True进行训练,因为这些交叉验证预测将被用作训练元模型的特征。

2.3. 将 XGBoost 和 LightGBM 作为基模型进行训练

接下来,让我们训练将形成堆叠集成基模型集合的 XGBoost 和 LightGBM 模型。我们将再次使用optuna来调优 H2O 的H2OXGBoostEstimator的超参数,并将所有训练过的模型记录在xgboost_lightgbm_models列表中。

在深入代码之前,我们必须首先了解H2OXGBoostEstimator是将流行的xgboost库中的 XGBoost 框架集成到 H2O 中。另一方面,H2O 并未集成lightgbm库。然而,它确实提供了一种方法,通过在H2OXGBoostEstimator中使用特定参数集来模拟 LightGBM 框架——这正是我们将实现的,以便使用H2OXGBoostEstimator来训练 XGBoost 和 LightGBM 模型。

xgboost_lightgbm_models = []

def objective(trial):
    #common params between xgboost and lightgbm
    params = {
        'ntrees': trial.suggest_int('ntrees', 50, 5000),
        'max_depth': trial.suggest_int('max_depth', 1, 9),
        'min_rows': trial.suggest_int('min_rows', 1, 5),
        'sample_rate': trial.suggest_float('sample_rate', 0.8, 1.0),
        'col_sample_rate': trial.suggest_float('col_sample_rate', 0.2, 1.0),
        'col_sample_rate_per_tree': trial.suggest_float('col_sample_rate_per_tree', 0.5, 1.0)
    }

    grow_policy = trial.suggest_categorical('grow_policy', ['depthwise', 'lossguide'])

     #######################################################################################################################
     #from H2OXGBoostEstimator's documentation, (https://docs.h2o.ai/h2o/latest-stable/h2o-docs/data-science/xgboost.html) # 
     #lightgbm is emulated when grow_policy=lossguide and tree_method=hist                                                 #
     #so we will tune lightgbm-specific hyperparameters when this set of hyperparameters is used                           #
     #and tune xgboost-specific hyperparameters otherwise                                                                  #
     #######################################################################################################################

    #add lightgbm-specific params
    if grow_policy == 'lossguide':  
        tree_method = 'hist'  
        params['max_bins'] = trial.suggest_int('max_bins', 20, 256)
        params['max_leaves'] = trial.suggest_int('max_leaves', 31, 1024)

    #add xgboost-specific params
    else:
        tree_method = 'auto'
        params['booster'] = trial.suggest_categorical('booster', ['gbtree', 'gblinear', 'dart'])
        params['reg_alpha'] = trial.suggest_float('reg_alpha', 0.001, 1)
        params['reg_lambda'] = trial.suggest_float('reg_lambda', 0.001, 1)
        params['min_split_improvement'] = trial.suggest_float('min_split_improvement', 1e-10, 1e-3, log=True)

    #add grow_policy and tree_method into params dict
    params['grow_policy'] = grow_policy
    params['tree_method'] = tree_method

    #train model
    model = H2OXGBoostEstimator(**params,
                                learn_rate=0.1,
                                categorical_encoding='auto',  #h2o models can do this feature preprocessing automatically
                                nfolds=5,
                                keep_cross_validation_predictions=True,  #need this for training the meta-model later
                                seed=seed) 
    model.train(x=x, y=y, training_frame=train)

    #store model
    xgboost_lightgbm_models.append(model)

    #get cross-validation rmse
    cv_metrics_df = model.cross_validation_metrics_summary().as_data_frame()
    cv_rmse_index = cv_metrics_df[cv_metrics_df[''] == 'rmse'].index
    cv_rmse = cv_metrics_df['mean'].iloc[cv_rmse_index]
    return cv_rmse

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=20)

类似地,20 个 XGBoost 和 LightGBM 模型被训练并存储在xgboost_lightgbm_models列表中,以备后续作为堆叠集成的基模型使用,每个模型有一组不同的超参数。你可以设置n_trials为任何你希望训练的 XGBoost/LightGBM 模型的数量。

重要的是,H2OXGBoostEstimator还必须使用keep_cross_validation_predictions=True进行训练,因为这些交叉验证预测将被用作训练元模型的特征。

2.4. 训练元模型

我们将使用上述训练的所有深度神经网络、XGBoost 和 LightGBM 模型作为基模型。然而,这并不意味着所有这些模型都会被用于堆叠集成,因为在调优我们的元模型时,我们将进行自动基模型选择(稍后会详细介绍)!

记住,我们将每个训练过的基模型存储在dnn_models(20 个模型)和xgboost_lightgbm_models(20 个模型)列表中,总共为我们的堆叠集成提供了 40 个基模型。接下来,我们将它们合并成一个最终的基模型列表base_models

base_models = dnn_models + xgboost_lightgbm_models

现在,我们已经准备好使用这些基模型来训练元模型。但首先,我们必须决定元模型算法,在此过程中涉及一些概念:

  1. 大多数关于堆叠集成的学术论文建议为元模型选择简单的线性算法。这是为了避免元模型对基模型的预测结果过拟合。

  2. H2O 推荐在回归任务中使用广义线性模型(GLM)而非线性回归,或在分类任务中使用广义线性模型而非逻辑回归。这是因为 GLM 是一种灵活的线性模型,不像后者那样强加正态性和同方差性的关键假设,这使得它能够更好地建模目标值的真实行为,因为这些假设在实际操作中可能很难满足。关于这一点的进一步解释可以在这篇学术论文中找到,H2O 的工作就是基于这篇论文的。

因此,我们将使用 H2OStackedEnsembleEstimator 并设置 metalearner_algorithm='glm' 来实例化元模型,并使用 optuna 调整 GLM 元模型的超参数以优化性能。

def objective(trial):
    #GLM params to tune
    meta_model_params = {
        'alpha': trial.suggest_float('alpha', 0, 1),  #regularization distribution between L1 and L2
        'family': trial.suggest_categorical('family', ['gaussian', 'tweedie']),  #read the documentation here on which family your target may fall into: https://docs.h2o.ai/h2o/latest-stable/h2o-docs/data-science/glm.html
        'standardize': trial.suggest_categorical('standardize', [True, False]),
        'non_negative': True  #predictions of each base model cannot be subtracted from one another
    }

    ensemble = H2OStackedEnsembleEstimator(metalearner_algorithm='glm',
                                             metalearner_params=meta_model_params,
                                             metalearner_nfolds=5,
                                             base_models=base_models,  
                                             seed=seed)

    ensemble.train(x=x, y=y, training_frame=train)

    #get cross-validation rmse
    cv_metrics_df = ensemble.cross_validation_metrics_summary().as_data_frame()
    cv_rmse_index = cv_metrics_df[cv_metrics_df[''] == 'rmse'].index
    cv_rmse = cv_metrics_df['mean'].iloc[cv_rmse_index]
    return cv_rmse

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=20)

注意,每个基础模型的交叉验证预测没有被显式地传递给 H2OStackedEnsembleEstimator。这是因为 H2O 在后台自动完成了这一步骤,使我们更加轻松!我们只需在之前训练基础模型时设置 keep_cross_validation_predictions=True,并使用参数 base_models=base_models 实例化 H2OStackedEnsembleEstimator 即可。

现在,我们可以最终构建 best_ensemble 模型,使用 optuna 找到的最佳超参数。

best_meta_model_params = study.best_params
best_ensemble = H2OStackedEnsembleEstimator(metalearner_algorithm='glm',
                                            metalearner_params=best_meta_model_params,
                                            base_models=base_models,
                                            seed=seed)

best_ensemble.train(x=x, y=y, training_frame=train)

完成了,我们成功地在 H2O 中训练了一个堆叠集成模型!让我们来看看吧。

best_ensemble.summary()

作者提供的图片

注意到堆叠集成模型只使用了我们提供的 40 个基础模型中的 16 个,其中 3 个是 XGBoost/LightGBM 模型,13 个是深度神经网络模型。这是因为我们为 GLM 元模型调整的超参数alpha,它代表了 L1(LASSO)和 L2(Ridge)之间的正则化分布。1 的值仅涉及 L1 正则化,而 0 的值仅涉及 L2 正则化。

如上所述,最佳值被发现为 alpha=0.16,因此使用了 L1 和 L2 的混合。一些基础模型的预测在 L1 正则化下的回归中系数被设置为 0,这意味着这些基础模型在堆叠集成中完全没有被使用,因此实际使用的基础模型数量少于 40 个。

这里的关键要点是,我们上面的设置还通过元模型的正则化超参数自动选择了用于最佳性能的基础模型,而不是简单地使用所有提供的 40 个基础模型。

3. 性能比较:堆叠集成与独立基础模型

为了展示堆叠集成的威力,让我们使用它为从一开始就被保留的验证集生成预测。下面的 RMSE 数字仅针对我使用的数据集,但也可以在自己的数据集上运行本文的代码,亲自查看模型性能的差异!

ensemble_val_rmse = best_ensemble.model_performance(val).rmse()
ensemble_val_rmse   #0.31475634111745304

堆叠集成在验证集上产生了 0.31 的 RMSE。

接下来,让我们深入分析每个基础模型在这个相同验证集上的表现。

base_val_rmse = []
for i in range(len(base_models)):
    base_val_rmse = base_models[i].model_performance(val).rmse()

models = ['H2ODeepLearningEstimator'] * len(dnn_models) + ['H2OXGBoostEstimator'] * len(xgboost_lightgbm_models)

base_val_rmse_df = pd.DataFrame([models, base_val_rmse]).T
base_val_rmse_df.columns = ['model', 'val_rmse']
base_val_rmse_df = base_val_rmse_df.sort_values(by='val_rmse', ascending=True).reset_index(drop=True)
base_val_rmse_df.head(15)  #show only the top 15 in terms of lowest val_rmse

图片由作者提供

与实现了 0.31 RMSE 的堆叠集成模型相比,表现最佳的独立基础模型的 RMSE 为 0.35。

这意味着堆叠方法能够在未见数据上提高预测性能 11%!

现在你已经见证了堆叠集成的威力,轮到你亲自尝试了!

写这篇文章我感到非常愉快,如果你阅读起来也觉得有趣,我会非常感激你花一点时间留下点赞和关注!

下次见!

Sheila

堆叠时间序列模型以提高准确性

原文:towardsdatascience.com/stacking-time-series-models-to-improve-accuracy-7977c6667d29

从 RNN、ARIMA 和 Prophet 模型中提取信号,以便用 Catboost 进行预测

Michael KeithTowards Data Science Michael Keith

·发表于Towards Data Science ·阅读时长 7 分钟·2023 年 2 月 28 日

--

图片由Robert Sachowski拍摄,发布在Unsplash上。

关于强大的时间序列模型的研究非常丰富。可供选择的选项很多,远远超出了传统的 ARIMA 技术。最近,递归神经网络和 LSTM 模型已成为许多研究人员关注的重点。根据PePy上列出的下载数量,Prophet 模型可能是时间序列预测者使用最广泛的模型,因为它的入门门槛较低。对于你的时间序列来说,哪种选项最合适呢?

也许答案是你应该尝试所有这些模型,并结合它们的各种优点。一个常见的技术是堆叠。流行的机器学习库 scikit-learn 提供了一个StackingRegressor,可以用于时间序列任务。我之前演示了如何使用它来处理这种情况。

然而,StackingRegressor 有一个限制;它只接受其他 scikit-learn 模型类和 API。因此,像 ARIMA 这样的模型(在 scikit-learn 中不可用)或来自 tensorflow 的深度网络将无法放入堆栈中。这里有一个解决方案。在这篇文章中,我将展示如何使用 scalecast 包和一个 Jupyter notebook 扩展时间序列的堆叠方法。使用的数据集可以在 GitHub 上 open access 获取。需要以下要求:

pip install --upgrade scalecast
conda install tensorflow
conda install shap
conda install -c conda-forge cmdstanpy
pip install prophet

数据集概述

数据集是按小时划分的,分为训练集(700 个观测值)和测试集(48 个观测值)。我的 notebook 使用了 H1 系列,但修改为利用 M4 数据集中任何一个小时序列是很直接的。这是如何读取数据并将其存储在 Forecaster 对象中的:

import pandas as pd
import numpy as np
from scalecast.Forecaster import Forecaster
from scalecast.util import metrics
import matplotlib.pyplot as plt
import seaborn as sns

def read_data(idx = 'H1', cis = True, metrics = ['smape']):
    info = pd.read_csv(
        'M4-info.csv',
        index_col=0,
        parse_dates=['StartingDate'],
        dayfirst=True,
    )
    train = pd.read_csv(
        f'Hourly-train.csv',
        index_col=0,
    ).loc[idx]
    test = pd.read_csv(
        f'Hourly-test.csv',
        index_col=0,
    ).loc[idx]
    y = train.values
    sd = info.loc[idx,'StartingDate']
    fcst_horizon = info.loc[idx,'Horizon']
    cd = pd.date_range(
        start = sd,
        freq = 'H',
        periods = len(y),
    )
    f = Forecaster(
        y = y, # observed values
        current_dates = cd, # current dates
        future_dates = fcst_horizon, # forecast length
        test_length = fcst_horizon, # test-set length
        cis = cis, # whether to evaluate intervals for each model
        metrics = metrics, # what metrics to evaluate
    )

    return f, test.values

f, test_set = read_data()
f # display the Forecaster object

这是该序列的图表:

作者提供的图片

应用模型

在开始堆叠模型之前,我们需要从它们生成预测。我选择使用简单模型作为 ARIMA、LSTM 和 Prophet 模型的基准。在接下来的部分中,我将解释为什么做出每个选择,但也可以做出其他决策,这些决策同样有趣,甚至更具吸引力。

简单基准测试

为了对所有模型进行基准测试,我们可以调用季节性简单估计方法,该方法将给定小时序列中的最后 24 个观测值向前传播。在 scalecast 中,这非常简单。

f.set_estimator('naive')
f.manual_forecast(seasonal=True)

ARIMA

自回归积分滑动平均是一种流行且简单的时间序列技术,它利用序列的滞后值和误差以线性方式预测未来。通过探索性数据分析(您可以在链接的 notebook 中看到),我确定序列不是平稳的,并且具有很强的季节性。我最终选择应用了一个季节性 ARIMA 模型,订单为 (5,1,4) x (1,1,1,24)。

f.set_estimator('arima')
f.manual_forecast(
    order = (5,1,4),
    seasonal_order = (1,1,1,24),
    call_me = 'manual_arima',
)

LSTM

如果 ARIMA 属于时间序列模型中的简单方法,LSTM 是更先进的方法之一。这是一种深度学习技术,具有许多参数,包括一个注意力机制,可以在序列数据中找到长期和短期的模式,这使其理论上成为时间序列的理想选择。使用 tensorflow 设置这个模型很困难,但在 scalecast 中并不太难(请参见 这篇文章)。我应用了两个 LSTM 模型:一个使用 Tanh 激活函数,一个使用 ReLu。

f.set_estimator('rnn')
f.manual_forecast(
    lags = 48,
    layers_struct=[
        ('LSTM',{'units':100,'activation':'tanh'}),
        ('LSTM',{'units':100,'activation':'tanh'}),
        ('LSTM',{'units':100,'activation':'tanh'}),
    ],
    optimizer = 'Adam',
    epochs = 15,
    plot_loss = True,
    validation_split=0.2,
    call_me = 'rnn_tanh_activation',
)

f.manual_forecast(
    lags = 48,
    layers_struct=[
        ('LSTM',{'units':100,'activation':'relu'}),
        ('LSTM',{'units':100,'activation':'relu'}),
        ('LSTM',{'units':100,'activation':'relu'}),
    ],
    optimizer = 'Adam',
    epochs = 15,
    plot_loss = True,
    validation_split=0.2,
    call_me = 'rnn_relu_activation',
)

Prophet

尽管 Prophet 模型极受欢迎,但它最近却被贬低。有人声称它的准确性令人失望,主要是因为它对趋势的外推过于不现实,并且它没有通过自回归建模考虑局部模式。然而,它确实有一些独特之处。比如,它会自动将节假日效应应用到模型中。它还考虑了几种季节性类型。它以用户需要的最少规格完成这些功能。我喜欢将它作为一个信号使用,即使它不适合生成点预测。

f.set_estimator('prophet')
f.manual_forecast()

比较结果

现在我们已经为每个模型生成了预测结果,让我们看看它们在验证集上的表现,这些是我们训练集中的最后 48 个观测值(这仍然与之前提到的测试集分开)。

results = f.export(determine_best_by='TestSetSMAPE')
ms = results['model_summaries']
ms[
    [
        'ModelNickname',
        'TestSetLength',
        'TestSetSMAPE',
        'InSampleSMAPE',
    ]
]

图片由作者提供

好消息是每个模型都优于朴素方法。ARIMA 模型表现最佳,百分比误差为 4.7%,其次是 Prophet。让我们查看所有预测与验证集的对比图:

f.plot(order_by="TestSetSMAPE",ci=True)
plt.show()

图片由作者提供

所有这些模型在这个系列上表现得相当合理,之间没有大的偏差。让我们进行模型叠加!

模型叠加

每个叠加模型都需要一个最终估计器,该估计器将筛选其他模型的各种估计值,以创建一组新的预测。我们将用一个叫做Catboost的增强树估计器来叠加我们之前探索的结果。Catboost 是一个强大的程序,我们希望它能从每个已经应用的模型中提取最佳信号。

 f.add_signals(
    f.history.keys(), # add signals from all previously evaluated models
)
f.add_ar_terms(48)
f.set_estimator('catboost')

上述代码将每个评估模型的预测结果添加到Forecaster对象中。它将这些预测称为“信号”。这些信号与存储在同一对象中的其他协变量一样对待。我们还将最后 48 个系列的滞后添加为 Catboost 模型可以用来进行预测的额外回归量。现在我们将调用三个 Catboost 模型:一个使用所有可用的信号和系列滞后,一个仅使用信号,还有一个仅使用滞后。

f.manual_forecast(
    Xvars='all',
    call_me='catboost_all_reg',
    verbose = False,
)
f.manual_forecast(
    Xvars=[x for x in f.get_regressor_names() if x.startswith('AR')], 
    call_me = 'catboost_lags_only',
    verbose = False,
)
f.manual_forecast(
    Xvars=[x for x in f.get_regressor_names() if not x.startswith('AR')], 
    call_me = 'catboost_signals_only',
    verbose = False,
)

让我们利用在分析开始时与 Forecaster 对象分开的测试集,比较所有应用模型的结果。这次,我们将关注两个指标:SMAPE 和平均绝对尺度误差(MASE)。这两个指标在实际的 M4 竞赛中被使用。

test_results = pd.DataFrame(index = f.history.keys(),columns = ['smape','mase'])
for k, v in f.history.items():
    test_results.loc[k,['smape','mase']] = [
        metrics.smape(test_set,v['Forecast']),
        metrics.mase(test_set,v['Forecast'],m=24,obs=f.y),
    ]

test_results.sort_values('smape')

图片由作者提供

通过结合来自不同类别模型的信号,我们生成了两个优于其他模型的估计器:一个使用所有信号和 48 个系列滞后的 Catboost 模型,以及一个仅使用信号的 Catboost 模型。这两个模型的误差大约为 2.8%。我们可以看到这两个模型与测试集中的实际数据进行对比的图示。

fig, ax = plt.subplots(figsize=(12,6))
f.plot(
    models = ['catboost_all_reg','catboost_signals_only'],
    ci=True,
    ax = ax
)
sns.lineplot(
    x = f.future_dates, 
    y = test_set, 
    ax = ax,
    label = 'held out actuals',
    color = 'darkblue',
    alpha = .75,
)
plt.show()

作者提供的图片

哪些信号最为重要?

为了完善分析,我们可以使用 Shapley 分数来确定在这个模型堆叠中哪些信号最为重要。Shapley 分数被认为是确定输入在给定机器学习模型中的预测能力的最先进方法之一。分数越高,表示这些输入在该模型中越重要。

f.export_feature_importance('catboost_all_reg')

作者提供的图片

上面的截图仅显示了前几个最重要的预测因子,但我们可以从中看出,ARIMA 信号最为重要,其次是系列的第一个滞后项,然后是 Prophet 信号。RNN 模型的得分也高于许多包含的滞后项。如果我们希望将来训练一个更简洁的模型,这可以是一个很好的起点。

结论

在这篇文章中,我展示了在时间序列上下文中堆叠模型的威力,以及如何通过使用多样的模型类别提高探索系列的准确性。所有这些都通过 scalecast 包轻松实现。如果你觉得这个分析有趣,请在GitHub上给这个包一个星标。感谢你的关注!

StackOverflow 的转型:从颠覆到机遇

原文:towardsdatascience.com/stackoverflows-pivot-from-disruption-to-opportunity-8831dfbd9df?source=collection_archive---------10-----------------------#2023-08-14

OverFlowAI 利用公司的核心资产,将答案呈现在一个可用的界面中,并创建一个生成 AI 循环来创造新内容

Viggy BalagopalakrishnanTowards Data Science Viggy Balagopalakrishnan

·

关注 发表在 Towards Data Science ·9 min read·2023 年 8 月 14 日

--

图片来源:Pakata GohUnsplash

随着像 GPT4 这样的高效模型推动生成性 AI 的发展,数据专业人士如何为他们所参与的组织提供长期价值正在演变。真正的价值不仅来自于成为房间里最具技术能力的人,还包括能够塑造这些技术如何影响产品和业务成果。这包括能够引导你的组织制定正确的数据战略,并塑造数据产品如何无缝地融入产品体验。本文对 StackOverflow 变革的分析提供了一个有力的案例研究,以实现这一目标。

StackOverflow 是软件开发人员获取编程支持的最常用平台,最近经历了一些波折。如果你以前没有使用过 StackOverflow,它是一个类似于 Quora 或 Reddit 的问答论坛,你可以在这里提出与编程相关的问题。虽然我已经好几年没有写过生产级代码,但在我写代码的时候,StackOverflow 确实非常棒。例如,如果你在编译代码时遇到了最晦涩的错误,并且收到了一条你无法理解的错误消息,你会将其放入 Google 搜索中。通常,你会找到一个 StackOverflow 页面,上面有人问了同样的问题并得到了回答。更少见的是,你会发现另一个人遇到了完全相同的晦涩问题但没有得到答案——在这种情况下,祝好运。更准确地说,69% 的问题 在 StackOverflow 上得到了回答,这确实很令人印象深刻。

StackOverflow 首页

然而,最近 StackOverflow 的流量一直在下降。Similarweb 的数据表明,他们的流量 同比下降了 14%(StackOverflow 表示 更接近 5%)。尽管如此,这一趋势仍然是下降的,主要原因是 AI 编程产品如 ChatGPT 和 GitHub Copilot 的出现。这些产品具有有意义的代码编写能力,因此能够提供编程支持,至少在某种程度上不亚于 StackOverflow。具有讽刺意味的是,支撑这些 AI 产品的几个大型语言模型(LLMs)是使用爬取的 StackOverflow 数据进行训练的。

这些发展使得公司受到了相当严厉的媒体报道。Business Insider 在他们的文章 死亡于 LLM 中写道:

欢迎来到 AI 世界中的互联网未来。像 Stack Overflow 和 Wikipedia 这样的在线社区曾作为专家和好奇浏览者聚集并自由分享信息的中心而繁荣。现在,这些数字聚会场所正被大科技公司掠夺,用于训练他们的大型语言模型。

从这次生成型 AI 繁荣中出现的新产品正在使这些在线论坛的未来充满疑问。聊天机器人可以清晰、自动且通常愉快地回答问题——因此人们无需与其他人接触即可获取信息。

在所有这些关注中,StackOverflow 采取了稳健的措施,并阐明了他们应对这一挑战的双管齐下的方法:

  1. 几周前,他们宣布将开始向使用平台上 5000 万+ 问题和答案进行模型训练的大型 AI 开发者收费(我们在之前的数据抓取文章中讨论了这个问题)

  2. 上周,他们推出了OverflowAI产品,这是一组实际有用的生成型 AI 功能,可以帮助他们开启第二个篇章——我们今天将重点关注这一点

在本文中,我们将深入探讨:

  • AI 代码编写工具颠覆 StackOverflow

  • OverflowAI 的功能

  • StackOverflow 策略中的潜在趋势

AI 代码编写工具颠覆 StackOverflow

当前市场上有几种 AI 代码编写和编辑工具。这些工具要么是独立的产品(如 OpenAI Codex、ChatGPT、Google Bard),要么是原生集成在现有平台中的产品(如 GitHub Copilot、Replit Ghostwriter、Amazon CodeWhisperer)。它们具有广泛的功能,包括代码生成、代码编辑、自动完成和调试。

原生分发的产品(如 GitHub Copilot)具有很大的优势,因为它们可以在程序员今天已经使用的环境中无缝操作,我们将看到更多产品尝试集成到现有环境中。例如,CodeGPT 有一个插件,可以让开发人员从Visual Studio Code(一个流行的代码编辑工具)中使用该产品。

现有的 AI 代码编写工具在某些任务上表现良好。例如,这个Reddit 线程记录了几位网页开发人员关于 GitHub Copilot 的反馈——总体主题是,该产品在开发人员需要编写全新代码并且不想从头开始编写的特定情况下非常有用。即便在这些情况下,它也经常会有成功和失败。

原因并不令人惊讶。从概念上讲,大型语言模型(LLMs)接收大量数据,并基于这个结构生成输出:在特定上下文中,对于你问的问题,最可能的单词/文本是什么。它本质上是在计算一个单词跟随另一个单词的概率,并基于此生成输出。尽管有这一结构,鉴于用于训练这些模型的数据量,ChatGPT 在更一般的用例(如起草电子邮件或总结页面)中的结果仍然令人印象深刻。但重要的是要记住,语言模型在设计上具有有限的分析/数学能力。换句话说,当你问模型“2+2 是多少”时,它可能会给出正确答案——不是因为它知道数学,而是因为它在训练数据中见过这个文本模式。

同样,在代码生成方面,模型并不真正“了解”编程背后的基本概念,而是基于大量文本数据的训练来预测结果。这导致了上面提到的 GitHub Copilot 反馈——它有时能生成你需要的基础代码,但其实际理解代码、调试和提供解释的能力有限。这随着时间的推移会有所改善,但很难说它是否会达到高准确性/高可靠性的程度。

StackOverflow CEO Prashanth Chandrasekar 简洁地描述了

现代 LLM 系统的一个问题是,它们会以与正确答案相同的信心提供错误答案,并且如果感觉符合用户寻求的答案模式,它们会“虚构”事实和数据。

在某些时候,你需要了解自己在构建什么。你可能需要调试它,但对刚刚构建的内容毫无头绪,并且很难通过捷径跳过学习过程。

这就是 StackOverflow 的机会——他们的流量下降可能是永久性的,程序员可能更少地访问 StackOverflow 以解决简单问题(例如,他们可能不再访问 StackOverflow 以获取现成的排序算法)。但产品能够闪光的地方是:1)对语言模型可能无法回答的复杂问题提供高准确性/高可靠性答案,以及 2)对语言模型没有先前数据进行训练的新技术/问题领域提供答案。OverflowAI 的设计就是为了直接抓住这个机会。

OverflowAI 的作用

他们押注的三个关键方面是——直接回答问题、在开发环境中的可用性,以及提升企业内的知识。

OverflowAI 搜索提供了以问答格式直接回答用户(类似于 ChatGPT),但提供了多个实际 StackOverflow 帖子的链接。除了帮助建立信任外,这还为用户提供了深入了解的机会,当 AI 提供的答案无法完全解决用户的问题时。这在简单问题时提供直接答案,而在复杂问题时引导用户沿着更探索性的路径前进,达到了微妙的平衡。

OverflowAI 搜索(来源:OverflowAI 演示视频

如果用户对回应不满意,他们可以进入一个聊天式界面以询问后续问题。如果答案都不令人满意,他们可以请求 StackOverflow代他们起草问题,准备好发布到问答论坛。这种体验还能避免用户提问时,问题已经被回答过的半常见情况。

自动问题草稿(来源:OverflowAI 演示视频

该产品还通过扩展在 Visual Studio Code 中提供所有这些功能,进一步提升了可用性。这帮助 StackOverflow 更有效地与原生集成的编码助手竞争,使开发人员能够在编码环境中获取答案(而不是需要切换上下文并从浏览器中搜索)。

Visual Studio Code 内的扩展(来源:OverflowAI 演示视频

除此之外,对于企业客户,OverflowAI 正在创建将公司内部多个不同信息来源(内部问答、维基页面、文档库)集成的能力,为开发人员提供一个连贯的问答体验。能够利用内部和 StackOverflow 数据,并且将其轻松地展示在问答式界面中,对于工程组织来说,可以大大提高生产力。他们还计划推出一个 Slack 集成,以无缝界面展示这一功能。

OverFlowAI 的产品方法令人印象深刻,它利用公司核心资产(对难题的回答),在用户所在的任何地方(无论是 Slack 还是开发环境中)提供高度可用的界面,并创造了一个用户可以利用生成式 AI 提交新问题的循环。

StackOverflow 策略中的潜在趋势

StackOverflow 并不是一个公开上市的公司——它由 Prosus 所拥有,Prosus 又是更大控股公司 Naspers 的一部分,而 Naspers 是上市公司。因此,很难获得准确的收入数据,但 Prosus 在2022 年 5 月发布的报告提供了一些线索:

  • 公司在 2022 年的收入约为 8900 万美元,其中 50%来自企业产品 StackOverflow for Teams,50% 来自 Reach 产品(广告和雇主品牌)。

  • 从 2021 年到 2022 年,StackOverflow for Teams 的收入增长了 69%,而 Reach 产品的收入下降了 12%(2022 年的收入可能受到其他因素的影响,如招聘放缓)。

来自 StackOverflow 所有者 Prosus 的收入数据(报告于 2022 年 5 月

这些收入数据结合 OverflowAI 产品的功能指向了 StackOverflow 在生成式 AI 领域的发展趋势(这些趋势也可以扩展到其他问答平台):

  1. 他们的广告业务,其成功直接与流量挂钩,正在下滑。这并不一定意味着严重问题,只是指出了一个更广泛的趋势——消费者会直接获得更简单问题的答案(这很好),因此广告成为了一个不那么关键的收入来源

  2. StackOverflow 将继续成为解决困难问题的宝贵来源,并且问题和答案的数量将随着公司推动生成式 AI 自动草拟/提交问题而继续增长。此外,如果 StackOverflow 能够维持内容引擎的运转,平台上的内容质量将会提高,因为重复/简单的问题将不再是内容的最高量。

  3. StackOverflow 将加倍努力构建能够为用户提供最大价值的体验(如 OverflowAI 搜索和 Visual Studio Code 扩展),并专注于那些客户愿意为这些优质体验付费的产品线(例如 StackOverflow for Teams)。

  4. 数据许可项目,即他们向 AI 公司收费以用于培训其数据,将会加速发展。

所有趋势都指向一个方向,即 StackOverflow 正在成功转型进入公司下一个阶段,公司已经做出了正确的产品/业务投资,以应对可能的干扰。此外,他们还提供了宝贵的社区服务,并为其他问答平台制定了可供借鉴的操作手册。总体来说,我对他们的发展方向感到乐观,并相信这将激发未来蓬勃发展的内容生态系统。

🚀 如果你喜欢这篇文章,可以考虑订阅我的每周通讯 每周,我会发布一篇深度分析关于当前技术话题/产品策略的文章,阅读时间约为 10 分钟。最好的祝愿,Viggy。

Unpacked | Viggy Balagopalakrishnan | Substack

对当前技术和商业话题的深入分析,帮助你保持领先地位。每周送到你的收件箱…

Unpacked | Viggy Balagopalakrishnan | Substack

你应该在 FAANG 还是初创公司开始你的职业生涯?

原文:towardsdatascience.com/startup-vs-big-firm-how-will-your-data-science-skills-flourish-as-you-kickstart-your-career-7ba47f468e23

期望在学习环境和技能发展方面得到什么

Khouloud El AlamiTowards Data Science Khouloud El Alami

·发表于 Towards Data Science ·10 分钟阅读·2023 年 9 月 4 日

--

不确定是加入初创公司还是大型科技公司开始你的职业生涯?如果你正面临这个十字路口,这篇文章就是为你准备的!

我现在是 Spotify 的一名数据科学家,但三年前,我只是一个学生,并且在这个问题上挣扎了很久。

我是更适合在初创公司还是成熟公司工作呢?

毕竟这是一个重大决定!

但与选择一盒麦当劳鸡块还是最新的“大培根芝士麦克”(或者他们现在叫它们什么)不同,这个问题涉及的是为我的整个职业生涯设定轨迹。

当我在折磨自己思考这个问题时,我意识到我并不是唯一一个这样的人。实际上,很多人都面临着这个困境。

这篇文章是一个系列的第一篇,在这个系列中,我将从不同的角度讨论这个话题,所有这些角度都是你需要考虑的。

我将结合我在 Spotify 的个人经验以及我在过程中遇到的其他人的经验。

所以如果你正在职业生涯的第一个阶段,那么请确保关注我。

(这也有助于我在 Medium 上获得更多曝光,让那些能从我的故事中受益的人更容易找到它们)

为什么我们现在还要在乎这个问题?

因为公司的规模可以很好地指示你的职业生涯将如何随着时间的推移而发展。这在你刚刚迈入职业世界的初期尤其如此。

因此,了解你的目标是什么在确定哪个选项最符合这些目标方面变得至关重要

  • 你是否觉得自己可以全身心投入到各种混合的事物中,边学边做,在混乱中蓬勃发展,同时享受戴上多顶帽子的刺激?

  • 或者你可能更喜欢深入某一特定领域,从专家的指导中学习,并在明确定义的路线图上工作?

花点时间反思一下你希望从这段旅程中得到什么。你认为自己在哪方面会最成功?

没有对错之分。

但首先在评论区分享你的经历,我们来讨论一下吧!

在评论区分享一下你自己在这个话题上的经历吧!

无论你是数据科学新手还是老手,你的故事可能会成为别人灵感的源泉。我很想了解你的经历,我相信其他人也会感兴趣!

  • 也许有些轶事?

  • 意外的转折?

  • “我希望有人告诉我”时刻?

让我们将这不仅仅变成一个独白,而是一个社区对话!

让我告诉你我是如何做出选择的!

当我以实习生身份加入 Spotify 时,我第一次踏入了科技行业的企业世界,我不知道会有什么样的期待。

我在 2021 年 Spotify 办公室的第一天

我不断问自己,是否最好和大科技公司里的专家一起成长✨,还是宁愿在初创公司里做一个全能型人才🃏。

所以我联系了 Spotify 的数据科学家,询问他们在不同公司规模下的经历。我的朋友们也在其他地方实习,所以我也确保问了他们。

在与许多专业人士交流后,我意识到我在这两者之间的选择将会影响我的职业发展,具体体现在:

  • 技能发展 → 专注于一个领域或在多个领域中多样化。

  • 学习环境 → 指导条件、可用性及质量。

  • 职业发展与前景 → 晋升路径以及公司外的未来机会+薪酬。

  • 专业联系 → 在行业中建立一个强大的网络并获得外部曝光。

  • 影响潜力 → 通过我的贡献产生影响。

我已经收集了足够的数据,并且可以早早地权衡加入哪一方的利弊。

三年后,我对自己做出的决定充满信心(不仅仅是因为它是 Spotify)。

在这篇文章中,我将涵盖技能发展与学习环境部分。更具体地说,如何期望在初创公司或大公司中技能的成长,以及你可能获得的学习经历。

想听听我的个人见解吗?好吧,准备好!

在初创公司成为全能型人才

图片由Erik Mclean提供,来源于Unsplash

#1. 初创公司的数据科学家通常身兼多职

这意味着你可能会做数据工程、数据库架构、机器学习建模等。根据你的需求,你会做这些工作。

你可能会接近成为我们所称的全栈数据专家。这对你的职业生涯可能大有裨益,尤其是如果你希望走一条多才多艺的道路。

我一个在初创公司工作的朋友最初被聘为机器学习工程师。然而,她很快发现自己需要处理数据工程任务,因为这些任务在产品发布时非常紧急。

一周,她在构建数据库架构,下一周,她又回到微调机器学习模型上。

在初创公司,你可以处理的任务类型非常多样化。这个角色可能会如此动态,以至于你可能常常感觉自己每几天就要更换一次职业。

这对于你来说可能特别令人兴奋,如果你:

  • 🫶 强烈相信产品

  • 👷 在从零开始的环境中茁壮成长

  • 🤓 喜欢在各种方面工作

  • 👥 享受在小型快速发展的团队中工作

这种环境允许多样性。因此,如果你还在探索数据世界的广度或喜欢承担多样化的责任,初创公司可能是你的终极选择

#2. 你可能无法学到最佳实践,但你可以在过程中创造它们

初创公司通常资源有限,无法指导和培养新毕业生的技能。

这点尤其重要,因为如果你对自己做的事情没有信心,你可能会搞砸而没有人指出你的错误。

我一个在初创公司工作的朋友被抛到了一个没有指导的仪表板构建任务中。整个团队都是初级人员,当时没有数据负责人,因此学习阶段非常缓慢。

他最初的版本效率低下,但他努力优化。随着时间的推移,他的试错方法演变成了一套整个初创公司采纳的最佳实践。

在初创公司,你很可能需要通过试错来学习,这从长远来看是有回报和令人满意的。特别是当你看到自己劳动的成果时。

但在那之前,你首先得从错误中艰难地学习。如果没有人教你正确的方法,你可能会学到不良的做法,然后不得不重新构建一切。

#3. 这可能以专业化为代价

在初创公司,你可能会对业务有一个全面的了解,这确实是如此。你还会更好地理解一切是如何连接和融入拼图中的,这也确实是这样。

但请记住,这可能以专业化为代价。特别是如果你主要对数据科学感兴趣,而对数据工程或数据领域的其他方面不那么关心的话。

另一位在初创公司工作的朋友遇到了这个问题。他最初对机器学习充满热情,但他发现自己投入了商业分析、产品管理,甚至一些即将发布的研究论文的基准测试任务中。

他喜欢获得新技能,但也觉得他最初的专业方向在行动中渐渐丢失了。他仍然难以理解真正的机器学习工程师的角色是什么。

毕竟,名言确实说过“全才而无专才”。

这在一开始是如此,尤其是如果创业公司仍处于早期阶段。然而,随着创业公司成熟,机会将会出现,因为它开始为你已经填补的角色招聘人员。

在成熟企业中如美酒般 aging

图片由 Hermes Rivera 提供,在 Unsplash

#1. 如果你优先考虑专业化,成熟的公司可能是你最好的选择

你可以专注于在核心工作上获得专业知识,因为许多基础工作,从数据开发到技术执行,都是由专门的专家处理的。

在 Spotify, 我不必自己构建和启动 A/B 测试。工程师、产品经理和设计师都会参与其中。作为数据科学家,我们也参与其中,但我们专注于实验的数据方面,如指标、样本量计算及所有其他统计含义。

我们还可以与数据工程师合作,帮助他们捕捉更多数据并提高数据质量。我们自己不去做这些工作。

#2. 你还会早早学习到最佳实践,并与专家一起更快成长

根据公司雇佣的才俊来挑选公司可以为你提供无与伦比的导师机会。这就像是在专业领域中的常春藤联盟。最棒的是?他们支付你去学习,而不是相反。

在 Spotify 的每一天, 我都会直接接受顶级数据科学家多年经验的指导。我每天都感激能从最优秀的人那里学习,这是我最看重的。我喜欢认为我正在专业领域中的哈佛。

如果你的目标是迅速且充分地发展你的技能,这一点非常重要!

看看你!(图片由作者提供,Midjourney)

#3. 你可能会被限制在某个特定的领域细分中

你可能没有太多机会探索该领域的其他方面,因为专门的专家已经在处理这些事情。

因此,你必须主动跟上领域中的最新动态,特别是考虑到人工智能的发展速度。你可不希望 30 岁时像奶奶一样对技术一无所知。

我保持在这个领域中的状态所做的事情:

  • 关注数据科学和人工智能的最新趋势。 Towards Data Science 做出贡献也帮助我跟上领域中的动态。

  • 与其他数据专业人士合作,如数据工程师和机器学习工程师,并对他们的角色保持好奇。

    如果机会没有出现,主动去创造它。尝试把这个想法带到你的团队面前,或者联系这些人并安排与他们的会议。人们总是乐意告诉你关于他们自己的事情!

#4. 他们有既定的协议、程序和基础设施

这些已经由那些知道自己在做什么的人铺设好了。

这意味着你从一开始就已经融入了良好的工作方式。这些工作方式将对你的整个职业生涯有价值。

如果有一天你醒来,意识到你想加入一家初创公司,你可能会对他们产生巨大附加价值,因为你将带来一大箱的好实践。

这也意味着你有更多的工具可以使用。

— 你不必从头开始创建数据仓库,它已经存在。— 你可能不需要手动设计 A/B 测试,可能已经有工具为你优化了这个过程。

在 Spotify,我们使用内部平台来启动我们的实验。这使我们能够在平台上直接创建、运行、管理和分析实验,而无需在笔记本中逐项处理。

重要的事情要考虑!!

许多这些考虑因素广泛适用,但在不同公司之间有所不同。

我在 Spotify 工作,它自 2006 年以来已经存在,所以我们可以称之为一家成熟公司,但一些经验方面仍然与初创公司相似。我们的 CHRO Katarina Berg 最准确地描述了“受控混乱”

这就是为什么评估这些因素在每个公司基础上是必不可少的。

如何? 联系你感兴趣的公司的某个人,询问他们的成长机会和学习环境。

你还可以在面试中使用这些作为深刻的问题,特别是如果你不确定该问什么。

最终,我加入了一家更成熟的公司,因为我相信这是对我来说的最佳决定——我想专注于数据科学。我并不一定对涉足所有数据相关领域如数据工程感兴趣。

但再说一次,这个选择是非常个人化的,你的选择也应该如此!

📌 附言:如果你发现这篇文章很有见地,请给我留个便条或评论,我很想知道!

我有礼物送给你🎁!

注册我的通讯 K’s DataLadder,你将自动获得我的终极 SQL 速查表,包含我在大科技公司每天使用的所有查询+另一个秘密礼物!

我每周分享作为数据科学家在科技领域的工作经验,提供实用技巧、技能和故事,旨在帮助你提升——因为没人真正知道,直到他们亲身经历!

如果你还没有做到这一点

  • 订阅我的YouTube频道。新视频很快就会发布!

  • 关注我在InstagramLinkedInX上的动态,选择你喜欢的方式。

很快见!

时间序列的平稳性——全面指南

原文:towardsdatascience.com/stationarity-in-time-series-a-comprehensive-guide-8beabe20d68

如何在 Python 中检查时间序列是否平稳,以及如果非平稳时可以做什么

Leonie MonigattiTowards Data Science Leonie Monigatti

·发表于Towards Data Science ·8 分钟阅读·2023 年 4 月 11 日

--

时间序列的平稳性(图由作者绘制)

未来更容易建模当它类似于现在时[3]。平稳性描述了时间序列的统计特征随着时间不会发生变化。因此,一些时间序列预测模型,如自回归模型,依赖于时间序列的平稳性。

在本文中,你将学到:

  • 什么是平稳性,

  • 为什么这很重要,

  • 检查平稳性的 3 种方法,以及

  • 当时间序列非平稳时可以应用的 3 种技术

什么是平稳性?

平稳性描述了一个概念,即时间序列如何变化将会在未来保持不变[3]。用数学术语来说,当时间序列的统计特性与时间无关时,它就是平稳的[3]:

  • 恒定均值,

  • 恒定方差,

  • 以及协方差与时间无关。

这就是弱平稳性的定义。另一种平稳性是严格平稳性。它意味着相同大小的样本具有相同的分布[5]。由于严格平稳性具有限制性且较为罕见,本文将仅关注弱平稳性。

为什么平稳性很重要?

一些时间序列预测模型(例如自回归模型)需要平稳的时间序列,因为由于其统计特性恒定,它们更易于建模[3]。因此,如果时间序列不平稳,你应该使其平稳(参见当时间序列不平稳时你可以做什么?)。

你如何测试平稳性?

你可以通过两种方式测试时间序列的平稳性:

  1. 直观方法:视觉评估

  2. 统计方法:单位根检验

在这一部分,我们将重现 Hyndman 和 Athanasopoulos [3] 用于解释平稳性视觉评估的一些示例,并扩展它们的使用,以解释使用单位根测试进行平稳性检验。数据取自相关的fma R 包 [1]。

展示平稳性测试的示例时间序列(作者提供的图像,受到 Hyndman 和 Athanasopoulos [3]的高度启发)

如何进行视觉平稳性评估

你可以通过心理上将时间序列分成两半来评估时间序列的平稳性,并比较前半部分与后半部分的均值、幅度和周期长度。

  • 常数均值 — 时间序列前半部分的均值应与后半部分相似。

  • 常数方差 — 时间序列前半部分的幅度应与后半部分相似。

  • 协方差与时间无关 — 时间序列前半部分的周期长度应与后半部分相似。周期应与时间无关(例如,不按周或月等)。

如何进行视觉平稳性测试(作者提供的图像)

对于我们的示例,评估结果如下所示:

在示例时间序列上的视觉平稳性评估(作者提供的图像,受到 Hyndman 和 Athanasopoulos [3]的高度启发)

如何统计性地评估平稳性 — 单位根检验的绕行

单位根是一个被称为“带漂移的随机游走”的随机趋势。由于随机性无法预测,这意味着:

  • 单位根存在:非平稳(不可预测)

  • 单位根不存在:平稳

要使用单位根检验进行平稳性测试,你需要以两种竞争假设的形式陈述你的初始假设 [6]:

  • 原假设(H0)— 例如,时间序列是平稳的(没有单位根)

  • 替代假设(H1) — 例如,时间序列是非平稳的(存在单位根)

然后你将根据两种方法来评估是否拒绝原假设:

  • p 值方法:

    如果 p 值 > 0.05,则未能拒绝原假设。

    如果 p 值 ≤ 0.05,拒绝原假设。

  • 临界值方法:

    如果检验统计量小于临界值,则未能拒绝原假设。

    如果检验统计量比临界值更极端,则拒绝原假设。

    当 p 值接近显著水平(例如,约为 0.05)时,应该使用临界值方法 [8]。

有几种单位根检验可以用来检查平稳性。本文将重点介绍最流行的几种:

  • 扩展的迪基-福勒检验 [2]

  • Kwiatkowski-Phillips-Schmidt-Shin 检验 [4]。

如何使用扩展的迪基-福勒检验来测试平稳性

扩展的迪基-富勒(ADF)测试的假设为[2]:

  1. 原假设(H0): 时间序列非平稳,因为存在单位根(如果 p 值 > 0.05)

  2. 备择假设(H1): 时间序列平稳,因为不存在单位根(如果 p 值 ≤ 0.05)

在 Python 中,我们可以使用statsmodels.tsa.stattools库中的adfuller方法[8]。

from statsmodels.tsa.stattools import adfuller

result = adfuller(df["example"].values)

如果我们能拒绝 ADF 测试的原假设,则时间序列是平稳的:

  • 如果 p 值(result[1])≤ 0.05

  • 如果测试统计量(result[0])比临界值(result[4]["1%"]result[4]["5%"]result[4]["10%"])更极端

使用 stattools Python 库进行 ADF 平稳性测试(图片由作者提供)

以下是样本数据集的 ADF 测试结果:

单位根(ADF 测试)对示例时间序列的平稳性评估(图片由作者提供)

如何使用 Kwiatkowski-Phillips-Schmidt-Shin 测试检验平稳性

Kwiatkowski-Phillips-Schmidt-Shin(KPSS)测试的假设为[4]:

  1. 原假设(H0): 时间序列平稳,因为不存在单位根(如果 p 值 > 0.05)

  2. 备择假设(H1): 时间序列非平稳,因为存在单位根(如果 p 值 ≤ 0.05)

这个统计量越大,我们就越可能拒绝原假设(即我们有一个非平稳时间序列)。

在 Python 中,我们可以使用statsmodels.tsa.stattools库中的kpss方法[9]。我们必须使用参数regression = 'ct'来指定测试的原假设是数据是趋势平稳的。[9]

from statsmodels.tsa.stattools import kpss

result = kpss(df["example"].values, 
              regression = "ct")

如果我们无法拒绝 KPSS 测试的原假设,则时间序列是平稳的:

  • 如果 p 值(result[1])> 0.05

  • 如果测试统计量(result[0])比临界值(result[3]["1%"]result[3]["2.5%"]result[3]["5%"]result[3]["10%"])不那么极端

使用 stattools Python 库进行 KPSS 平稳性测试(图片由作者提供)

以下是样本数据集的 KPSS 测试结果:

单位根(KPSS 测试)对示例时间序列的平稳性评估(图片由作者提供)

当时间序列非平稳时你可以做什么?

你可以对非平稳时间序列应用不同的变换以尝试使其平稳:

  • 差分

  • 通过模型拟合去趋势

  • 对数变换

由于平稳性有几种类型,我们可以结合使用 ADF 和 KPSS 测试来确定需要进行什么变换[7]:

  • 如果 ADF 测试结果为平稳而 KPSS 测试结果为非平稳,则时间序列是差分平稳——对时间序列进行差分处理,并再次检查平稳性[7]。

  • 如果 ADF 检验结果是非平稳的,而 KPSS 检验结果是平稳的,则时间序列是趋势平稳的——去趋势时间序列并再次检查平稳性 [7]。

差分

差分计算两个连续观察值之间的差值。它使时间序列的均值稳定,从而减少趋势 [3]。

df["example_diff"] = df["example"].diff()

差分后的时间序列(图像作者提供)

如果你想扩展对差分的了解,你应该查看 fractional differencing。

通过模型拟合去趋势

去除非平稳时间序列中的趋势的另一种方法是对数据拟合一个简单模型(例如线性回归),然后对该拟合的残差进行建模。

from sklearn.linear_model import LinearRegression

# Fit model (e.g., linear model)
X = [i for i in range(0, len(airpass_df))]
X = numpy.reshape(X, (len(X), 1))
y = df["example"].values
model = LinearRegression()
model.fit(X, y)

# Calculate trend
trend = model.predict(X)

# Detrend
df["example_detrend"] = df["example"].values - trend

通过模型拟合去趋势时间序列(图像作者提供)

对数转换

对数转换使时间序列的方差稳定 [8]。

df["example_diff"] = np.log(df["example"].value)

对数转换后的时间序列(图像作者提供)

如你所见,模型拟合去趋势以及单独的对数转换都没有使我们的示例时间序列平稳。你还可以结合不同的技术来使时间序列平稳:

通过模型拟合去趋势对数转换后的时间序列(图像作者提供)

摘要

在时间序列预测中,一个具有恒定统计属性(均值、方差和协方差)的时间序列,即与时间无关的时间序列,被描述为平稳的。

由于具有恒定的统计特性,平稳时间序列比非平稳时间序列更容易建模。因此,许多时间序列预测模型假设平稳性

平稳性可以通过视觉评估或统计方法来检查。统计方法检查单位根,这是非平稳性的一个指标。两个最流行的单位根检验是 ADF 和 KPSS。两者都可以在 Python 的stattools库中找到 [8,9]。

如果时间序列是非平稳的,你可以尝试通过差分、对数转换或去除趋势来使其平稳

喜欢这个故事吗?

免费订阅 以便在我发布新故事时收到通知。

[## 每当 Leonie Monigatti 发布时收到电子邮件。

每当 Leonie Monigatti 发布时收到电子邮件。通过注册,如果你还没有,你将创建一个 Medium 账户……

medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----8beabe20d68--------------------------------)

LinkedInTwitter Kaggle上找到我!

数据集

所有数据集均来自 fma R 包。

[1] Hyndman RJ (2023). fma: 取自“预测:方法与应用”由 Makridakis, Wheelwright & Hyndman (1998)编写的数据集。R 包版本 2.5,pkg.robjhyndman.com/fma/

许可证: GPL-3 (cran.r-project.org/web/packages/fma/index.html)

参考文献

[2] Dickey, D. A. 和 Fuller, W. A. (1979). 具有单位根的自回归时间序列估计值的分布。J. Am. Stat. Assoc. 74,427–431。

[3] R. J. Hyndman, & G. Athanasopoulos (2021) 预测:原理与实践,第 3 版,OTexts:墨尔本,澳大利亚。OTexts.com/fpp3。(访问于 2022 年 9 月 26 日)。

[4] Kwiatkowski, D., Phillips, P. C., Schmidt, P., & Shin, Y. (1992). 对单位根的替代理论进行平稳性零假设检验:我们对经济时间序列存在单位根的确定性有多高?《计量经济学杂志》,54(1–3),159–178。

[5] D. C. Montgomery, C. L. Jennings, Murat Kulahci (2015) 《时间序列分析与预测简介》,第 2 版,John Wiley & Sons。

[6] PennState (2023). S.3 假设检验(访问于 2022 年 9 月 26 日)。

[7] statsmodels (2023). 平稳性与去趋势(ADF/KPSS)(访问于 2023 年 3 月 10 日)。

[8] statsmodels (2023). statsmodels.tsa.stattools.adfuller(访问于 2022 年 9 月 26 日)。

[9] statsmodels (2023). statsmodels.tsa.stattools.kpss(访问于 2022 年 9 月 26 日)。

使用重采样的统计实验

原文:towardsdatascience.com/statistical-experiments-with-resampling-cb9ad2a5e66b?source=collection_archive---------5-----------------------#2023-08-02

自举法与置换检验

潘·克雷坦数据科学 潘·克雷坦

·

关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 8 月 2 日

--

图片来自 MollyroseleePixabay

引言

大多数从事数据工作的人会进行观察,然后思考这些观察是否具有统计学意义。除非有人接受过正式的统计推断培训,并且有过执行显著性检验的经验,否则首先想到的就是找一位统计学家,寻求如何进行检验的建议,或者至少确认检验是否正确执行以及结果是否有效。

这有很多原因。首先,通常不容易立即明确需要哪种测试,哪些公式支撑测试原理,如何使用这些公式,以及测试是否可以使用,例如,因为数据不满足如正态性等必要条件。对于估计各种统计模型和进行统计测试,有全面的 R 和 Python 包,例如 statsmodels

尽管如此,如果没有对统计理论的全面理解,使用包通过复制用户指南中的示例,常常会留下持续的不安,担心一旦方法被经验丰富的统计学家审查会受到严厉批评。就个人而言,我是一个随着时间的推移转变为数据分析师的工程师。我在本科和研究生阶段有过统计课程,但我并没有广泛使用统计学,因为这通常不是工程师的工作。我相信许多其他数据分析师和数据科学家也是如此,特别是如果他们的正式培训例如是在工程、计算机科学或化学方面。

我决定写这篇文章,因为我最近意识到模拟可以很容易地替代更经典的基于公式的统计方法。大多数人可能会立即想到自助法来估计均值的不确定性。但这不仅仅是自助法。使用在随机置换测试中进行重复抽样可以为许多统计推断问题提供答案。这类测试通常不难编写和执行。它们普遍适用于连续或二元数据,无论样本大小如何,也不对数据分布做假设。从这个意义上讲,置换测试是非参数的,唯一的要求是可交换性,即观察到某个值序列的概率对于该序列的任何置换都是相同的。这真的不算多要求。

计算资源的缺乏可能是过去基于公式的统计推断测试取得显著进展的原因之一。当时重复抽样数千次包含几十或几千条记录的数据样本是不可行的,但现在已不再不可行。这是否意味着经典的统计推断方法不再需要?当然不是。但能够进行置换测试并确认结果相似,或者帮助理解观察到的差异时哪些假设不成立,这可以令人安心。从头开始运行统计测试而不依赖于包也能带来一定的掌控感。

排列检验当然不是什么新鲜事,但我认为提供一些示例和相应的代码是个好主意。这可以缓解一些数据专家的恐惧,并将基于模拟的统计推断带入他们的日常实践。本文使用排列检验来回答两个问题。还有许多场景可以使用排列检验,对于更复杂的问题,排列检验的设计可能并不显而易见。从这个意义上说,本文并不全面。然而,原则是相同的。理解基础知识将更容易查找权威来源来设计排列检验以回答其他更微妙的商业问题。我的意图是激发一种思维方式,将模拟总体分布作为核心,并利用理论抽样来估计观察到的效应是偶然发生的概率。这就是假设检验的核心。

统计推断始于一个假设,例如,新药对某种疾病的疗效比传统治疗更佳。效果可以通过检查某个血液指标(连续变量)的减少情况,或通过统计接受新药和传统治疗(对照组)后无法检测到疾病的动物数量(离散变量)来测量。这样的两组比较,也称为 A/B 测试,在所有经典统计学教材和流行技术博客中都有广泛讨论,比如这个链接。以药物设计为例,我们将测试新药是否比传统治疗更有效(A/B 测试)。基于此,我们将估计需要多少动物来确认新药比传统治疗有效 1%(或其他效应大小)。虽然这两个问题看似无关,但其实并非如此。我们将重用第一个问题的代码来回答第二个问题。所有代码可以在我的博客仓库中找到。

我欢迎评论,但请保持建设性。我并不假装自己是统计学家,我的意图是帮助他人经历类似的学习过程,特别是在排列检验方面。

A/B 测试

回到第一个问题,即新药是否比传统治疗更有效。当我们进行实验时,生病的动物被分配到两个组中,具体取决于它们接受哪种治疗。动物被随机分配到各组,因此任何观察到的治疗效果差异要么是由于药物的有效性,要么是因为随机分配的结果使得免疫系统较强的动物被分配到了新药组。这两种情况是我们需要解开的。换句话说,我们想要检验随机机会是否能够解释使用新药所观察到的任何好处。

让我们设定一些假设的数字来做个说明:

响应变量是二元的,即治疗是否成功。置换检验在响应变量为连续变量时也能以相同方式工作(这与经典统计检验不同!),但上面的表格会包含均值和标准差,而不是计数。

我们故意不使用相同大小的处理组,因为这不是置换检验的要求。这次假设的 A/B 测试涉及了大量动物,似乎新药物很有前景。新药比传统治疗效果提高了 1.5%。鉴于样本量很大,这看起来很显著。我们会再讨论这个问题。作为人类,我们倾向于将可能不显著的事物视为显著。这就是标准化假设检验如此重要的原因。

“考虑零假设时,假设什么都没有发生,即机会可以解释一切。”

在 A/B 测试中,我们使用一个基线假设,即没有观察到任何特殊情况。这也被称为零假设。进行测试的人通常希望证明零假设不成立,即发现了某种效果。换句话说,替代假设是真实的。证明这一点的一种方法是显示随机机会导致观察到的差异的概率非常低。我们已经开始看到与置换检验的关联。

想象一个程序,其中所有处理的动物被汇总到一个单一的组(2487 + 1785 只动物),然后随机分成两个与原始处理组相同大小的组。对于每只动物,我们知道治疗是否成功,因此我们可以计算每组的治愈动物的百分比。使用观察到的数据,我们确定新药将治愈动物的百分比从 80.34% 提高到 81.79%,即增加了将近 1.5%。如果我们多次重新抽样这两个组,我们会看到新药导致治愈动物的百分比相比于传统治疗的频率是多少?这个“频率”就是统计推断中的普遍 p 值。如果这种情况经常发生,即 p 值大于我们觉得舒适的阈值(通常是 5% 的显著性水平),那么我们在实验中看到的结果可能是由于偶然性,因此零假设未被拒绝。如果这种情况很少发生,那么偶然性无法导致观察到的差异,因此零假设被拒绝(如果你的团队发现了新药,你可以举办一个派对!)。如果你仔细观察,我们实际上用置换来模拟零假设,即两个处理组是等效的。

再次思考零假设是如何被提出的,因为这决定了置换测试的进行方式。在上述例子中,我们想要了解偶然情况使我们相信替代假设(即新药更有效)会发生的频率。这意味着,与替代假设互补的零假设声明新药的效果低于或等于传统治疗。这也被称为单向检验(与双向检验,即双向检验相对)。换一种方式思考。我们不希望被偶然情况误导,认为新药更有效。相反方向的误导并不重要,因为我们无意取代传统治疗。双向检验会导致更高的 p 值,因此更加保守,因为它有更大的机会拒绝零假设。然而,这并不意味着如果这不是正确的检验方式,就应该使用它。

置换检验可以在最一般的情况下表述如下。假设有 Gᵢ, i=1,..,Nᴳ 组,每组的基数为 ∣ Gᵢ ∣, i=1,..,Nᴳ:

  • 将所有组中的数据点汇总在一起;这本质上是通过假设没有任何事件发生来模拟零假设。

  • 随机将 ∣ G₁ ∣ 个点分配到组 G₁ 中,不进行替换,将 ∣ G₂ ∣ 个点分配到组 G₂ 中,不进行替换,依此类推,直到所有点都已分配完毕。

  • 计算在原始样本中计算的感兴趣的统计量,并记录结果。

  • 重复上述过程大量次数,并记录每次感兴趣的统计量。

实质上,上述过程构建了一个包含感兴趣统计量的分布。观察到的值至少与观察到的差异一样极端的概率就是 p 值。如果 p 值较大,那么偶然很容易产生观察到的差异,我们尚未有发现(还)。

“将 p 值视为在零假设成立的情况下,观察到的结果极端程度的概率。”

上述公式相当通用。回到我们的例子,我们只有两组,一个是新药组,一个是传统治疗组。执行置换测试的代码如下。

我们进行 10,000 次置换,大约需要 30 秒。在我的机器上,关键问题是:偶然的情况下,新药比传统治疗更有效 1.5% 或更多的频率是多少?我们可以可视化模拟的效果差异的直方图,并计算下方显示的 p 值。

这给出了以下直方图:

新药与传统治疗在效果差异上的频率分布(作者提供的图片)

红色条形图表示新药比传统治疗偶然更有效的情况。这并不显得那么罕见。p 值为 0.1229。假设我们希望以 a=0.05 的显著性水平进行测试,这意味着零假设不能被拒绝。目前没有值得庆祝的事情。如果你组织了庆祝活动,需要取消或推迟。

“将 a 视为假阳性率,即假设零假设成立,我们会在 5% 的时间内得出统计显著差异的结论,如果我们重复进行实验。”

还是有一些乐观的理由。我们刚刚进行的 A/B 测试有两种可能的结果:要么存在效果(在我们的案例中,新药比传统治疗更有效),要么没有足够的证据来得出存在效果的结论。测试并没有得出没有效果的结论。新药仍可能更有效。只是我们目前无法在选择的显著性水平下用现有数据证明这一点。测试实质上保护我们免于假阳性(也称为第一类错误);但可能存在假阴性(也称为第二类错误)。这是团队所希望的。

我们还可以问另一个问题。观察到的差异需要达到多少才能得出新药比传统治疗更有效的结论?显然 1.5%是不够的,但多少才算足够呢?答案可以从生成的直方图中直接获得。我们可以将对应于观察到的差异的垂直线向右“移动”,直到红色条形的尾部占总面积的 5%;换句话说,使用 95%百分位数np.percentile(differences, 95),其值为 0.0203 或 2.03%。比我们观察到的 1.5%多一点,虽然不太理想,但也不算太离谱。

使用 0.05 的显著性水平,如果新药治疗效果的增加在区间(-∞, 0.0203]中,我们将不会拒绝原假设。这也被称为置信区间:观察到的统计量值的集合,这些值不会拒绝原假设。由于我们使用了 5%的显著性水平,这就是 95%的置信区间。假设新药没有更高的效率,那么多次进行实验将会在 95%的时间内得到置信区间内的效果差异。这就是置信区间告诉我们的信息。p 值将超过a,当且仅当置信区间包含观察到的效果增加,这意味着原假设不能被拒绝。这两种检查原假设是否可以被拒绝的方法当然是等效的。

以目前测试的动物数量,我们不能拒绝原假设,但我们距离置信区间边界还不远。团队持乐观态度,但我们需要收集更多有力证据以证明新药更有效。但需要多少更多的证据呢?我们将在下一节重新审视这个问题,因为运行重采样模拟也可以帮助我们回答这个问题!

在我们结束这一部分之前,重要的是要注意我们还可以使用经典统计测试来近似 p 值。上面展示的表格也被称为列联表,它提供了两个变量之间的相互关系,并且可以用来确定它们之间是否存在交互作用。可以使用从列联矩阵开始的卡方检验来检查这两个变量的独立性,但需要小心不要运行双侧检验(虽然没有广泛尝试,但 scipy 似乎默认使用双侧检验;这将导致更高的 p 值)。我们也可以使用Fisher 精确的单侧检验,这会得出一个 p 值为 0.124,与通过采样计算的值差异不大。增加置换次数可能会趋向于这个值。在深入了解统计库用户指南之前,了解如何运行置换检验岂不是很棒?

功效估计

由于我们无法证明新药的提高效果在统计上是显著的,因此人们可能会感到失望。新药可能确实更好。我们愿意通过处理更多的动物来进行更多的工作,但我们需要多少动物呢?这就是功效的作用所在。

功效是指在给定样本大小和显著性水平下检测到给定效应量的概率。假设我们期望新药将治疗效果提高 1.5%与传统治疗相比。假设我们已经用每种治疗处理了 3000 只动物,并将显著性水平固定为 0.05,那么检验的功效为 80%。这意味着如果我们重复实验多次,我们会发现 5 次实验中的 4 次我们得出新药比传统治疗更有效的结论。换句话说,假阴性率(Type II 错误)为 20%。上述数字当然是理论上的。重要的是,样本大小、效应量、显著性水平和功效这四个量是相关的,设置其中的任何三个可以计算出第四个。最典型的情况是从其他三个量计算样本大小。这就是我们在本节中要研究的内容。作为简化,我们假设在每个实验中我们用新药和传统治疗处理相同数量的动物。

下面的过程尝试构建一个以样本大小为函数的功效曲线:

  • 创建一个合成数据集,其中的动物假设已经接受了传统治疗,以便治疗效果或多或少地符合我们所知道的效果(下面,我将其设置为 0.8034,这与上面的列联矩阵相对应)。

  • 创建一个合成数据集,假设动物接受了新药治疗,通过添加我们希望调查的效应大小(如下所示,我将其设置为 0.015 和 0.020,以观察其对结果的影响)。

  • 从每个合成数据集中抽取大小为 n_sample 的自助样本(下文我将其设置为 3000、4000、5000、6000 和 7000)。

  • 使用我们在上一节中建立的方法进行置换检验,以确定治疗效果的差异是否具有统计学意义。

  • 不断生成自助样本并计算治疗效果的差异在多大程度上具有统计学意义;这就是检验的功效。

这当然是一个较长的模拟,因此我们将自助样本的数量限制为 200,而显著性检验中的置换次数也减少到 500,与上一节相比有所减少。

运行这个自助法/置换法模拟大约需要一个小时左右的时间,在普通计算机上可以受益于多进程,这超出了本文的范围。我们可以使用 matplotlib 轻松地可视化结果:

这生成了以下图表:

两种效应大小和 0.05 显著性水平下的样本量与统计功效的关系。图像由作者提供。

我们从中学到了什么?如果我们期望新药的有效性提高 1.5%,那么为了以 80%的把握证明这一点,我们需要治疗超过 7000 只动物。如果效应大小更大,即 2%,我们需要的样本量就会减少,大约 4500 只动物就足够了。这是直观的。检测到大效应比小效应要容易。决定进行这样的大规模实验需要进行成本/收益分析,但至少现在我们知道了证明新药更有效所需的条件。

我们还可以使用 statsmodels 计算所需的样本量:

这将打印:

effect size: 0.015, sample size: 8426.09
effect size: 0.020, sample size: 4690.38

模拟结果似乎一致。在模拟中,我们将样本量增加到 7000,但当效应大小为 1.5%时,样本量不足以达到 0.8 的把握,这一点也通过使用[proportion_effectsize](https://www.statsmodels.org/dev/generated/statsmodels.stats.proportion.proportion_effectsize.html)函数得到了验证。

结论

希望你喜欢这篇文章。我个人觉得,能够通过简单的自助法和置换法从零开始研究所有这些统计概念是非常充实的。

在我们结束之前,必须提醒注意。这篇文章非常强调 p 值,但 p 值正受到越来越多的批评。事实上,p 值的重要性在历史上被夸大了。p 值表示数据与代表零假设的统计模型或置换检验的不兼容程度。p 值不是替代假设真实的概率。此外,显示零假设可以被拒绝的 p 值并不意味着效应大小很重要。小的效应大小可能在统计上显著,但其效果如此微小,以至于不具实际意义。

参考文献

  • 《初级统计学与分析:重抽样视角》,作者:Peter Bruce(Wiley,2014)

  • 《数据科学家的实用统计学:使用 R 和 Python 的 50 多个基本概念》,作者:Peter BruceAndrew BrucePeter Gedeck(O’Reilly,2020)

  • 解释 A/B 测试结果:假阳性和统计显著性,Netflix 技术博客

统计独立性入门

原文:towardsdatascience.com/statistical-independence-for-beginners-69534ce04641

使用 R 和 Excel 函数的直观解释

Jae KimTowards Data Science Jae Kim

·发表于Towards Data Science ·5 分钟阅读·2023 年 3 月 21 日

--

图片由Naser TamimiUnsplash提供

统计独立性是统计学中的一个基本概念。它构成了许多(有监督的)机器学习算法的基础假设,如 logit 模型和朴素贝叶斯分类器。它也与人工智能中的关键方法紧密相关,如最大熵和神经网络。有关更多见解,请参见这篇文章

在这篇文章中,我通过直观解释、示例和统计独立性测试资源(R 代码和 Excel 函数)来说明其定义。

Probabilities

为简单起见,考虑两个事件 A 和 B,并定义以下概率:

Prob(A): 事件 A 的(边际)概率

Prob(B): 事件 B 的(边际)概率

Prob(A ∩ B): 事件 A 和 B 的联合概率,即 A 和 B 同时发生的概率;

Prob(A|B): 事件 A 在给定 B 的条件下的条件概率,即在事件 B 已发生的情况下,事件 A 的概率;

Prob(B|A): 事件 B 在给定 A 的条件下的条件概率。

这些概率之间的关系为

由作者创建的图片

如下方维恩图所示,Prob(A|B) 表示事件 A 对事件 B 的贡献比例(黄色区域:A ∩ B),以概率的形式

由作者创建的图片

例如,

A: 已婚,B: 男性

P(A|B) = 男性中的婚姻概率;

A: 失业,B: 大学毕业生

P(A|B) = 大学毕业生失业的概率

统计独立性

统计独立性有两个等效条件。首先,如果事件 A 和 B 是统计独立的

Prob(A ∩ B) = Prob(A) × Prob(B)

A 和 B 同时发生的概率是概率的乘积。这意味着如果它们同时发生,那纯粹是偶然的,两者之间没有系统性的关联

其次,如果事件 A 和 B 是统计独立的

Prob(A|B) = Prob(A)。

该条件来源于上述关系:

图片由作者创建

事件 B 的发生条件下的 A 的概率与 A 的概率相同。这意味着,在事件 B 已经发生的情况下,你对事件 A 的了解不会影响事件 A 的概率

类似地,Prob(B|A) = Prob(B)。

简单的例子

你连续抛两枚(公平的)硬币,每枚硬币显示 H(正面)或 T(反面),Prob(H) = Prob(T) = 0.5。你将会有以下结果:

(H, H), (H, T), (T, H), (T, T)。

例如,

Prob(H ∩ T) = 0.25,这等于

Prob(H) × Prob(T) = 0.5 × 0.5。

即,如果你得到结果(H, T),那纯粹是偶然的,没有系统性的关联。或者,

Prob(T | H) = 0.25 = Prob(T ∩ H)/P(H) = 0.25/0.5 = Prob(T)

如果你从第一枚硬币中得到 H,这对第二枚硬币得到 T 或 H 的概率没有影响。

现实世界的例子

A:已婚,B:男性

Prob(A ∩ B) = Prob(A) × Prob(B);随机选择一个人是男性且已婚,这种联合发生纯粹是偶然的。

Prob(A|B) = Prob(A);男性的婚姻概率与婚姻的概率相同。成为男性对婚姻概率没有影响。

测试独立性:卡方检验

进行了一项调查,以检查个人的婚姻状态与性别之间是否存在任何关联。在 100 名随机选择的个体中,40 名是男性,60 名是女性。其中 75 人已婚,25 人未婚。下表展示了婚姻状态和性别的联合频率。

图片由作者创建

例如,

Prob(Y ∩ M) = 25/100;Prob(M) = 40/100

Prob(Y|M) = Prob(Y ∩ M)/Prob(M) = 25/40

这些频率与统计独立下的预期频率进行比较:

图片由作者创建

在独立性下,预期的联合概率列如上:

例如,

Prob(Y ∩ M) = Prob(Y) × Prob(M) = 75/100 × 40/100 = 0.3;

Prob(Y ∩ F) = Prob(Y) × Prob(F) = 75/100 × 60/100 = 0.45。

实际频率与预期值类似,但它们是否足够接近以证明统计独立性?要检验统计独立性的零假设,我们需要进行独立性检验。

卡方检验广泛用于此目的,它比较实际频率(Oi)与预期频率(Ei)

图片由作者创建

其中 n 是表中单元格的数量,N 是总响应数,pi是独立性下的期望概率(或相对频率)。上述统计量遵循自由度为 df = (行数−1)×(列数−1)的卡方分布,其中行数和列数是表的行数和列数。

以下 R 代码显示了测试结果,包括检验统计量和 p 值。对象table定义为上述实际频率的 2 × 2 矩阵,并输入到函数chisq.test中。在 5%的显著性水平下,性别和婚姻之间的独立性零假设被拒绝,p 值为 0.018,检验统计量为 5.56。选项correct= FALSE与连续性修正有关,在这里没有使用,以与 Excel 函数保持一致。

table=matrix(c(25,50,15,10),nrow=2)
> table
     [,1] [,2]
[1,]   25   15
[2,]   50   10
> chisq.test(table,correct = FALSE)

 Pearson's Chi-squared test

data:  table
X-squared = 5.5556, df = 1, p-value = 0.01842

Excel 函数CHISQ.TEST返回检验的 p 值。该函数要求输入实际范围和期望范围,如下所示:

图像由作者创建

图像由作者创建

统计独立性是统计学、机器学习和人工智能中许多方法和概念的基础。它意味着两个或更多的随机变量没有系统的关联,即

  • 它们的联合发生完全是偶然的;或者

  • 一些随机变量的发生对其他变量的概率没有影响。

结果是,这些随机变量之间的相关性为零。

作为数据科学家,对这一基本概念有深入的理解非常重要。本文旨在提供统计独立性的直观解释,包括示例和计算资源。

使用 Julia 进行统计绘图:AlgebraOfGraphics.jl

原文:towardsdatascience.com/statistical-plotting-with-julia-algebraofgraphics-jl-4ead1507c638?source=collection_archive---------8-----------------------#2023-04-19

Phyto by Antoine Dautry on Unsplash

如何使用 AlgebraOfGraphics.jl(以及 Makie.jl)包创建统计图表

Roland SchätzleTowards Data Science Roland Schätzle

·

关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 4 月 19 日

--

图形语法(GoG)是一个理论概念,是许多流行图形包(如 R 中的 ggplot2 或 Python 中的 ggplot)的基础。在 Julia 生态系统中,甚至有几个基于 GoG 的图形包。因此,用户有选择的余地。因此,我创建了这一系列文章,以比较这些包,从而使选择变得更容易。

我以 GoG 介绍 开始了这一系列,并已介绍了图形包 [Gadfly.jl](http://gadflyjl.org/stable/) (使用 Julia 进行统计绘图:Gadfly.jl) 和 [VegaLite.jl](https://www.queryverse.org/VegaLite.jl/stable/) (使用 Julia 进行统计绘图:VegaLite.jl)。

[AlgebraOfGraphics.jl](https://aog.makie.org/stable/)-package (AoG) 现在是我在这一系列中介绍的基于图形语法(GoG)的 第三 个图形包。

对于本文章中演示 AoG 的示例,我将使用与之前文章中完全相同的数据(数据的详细说明可以在 这里 找到),并尝试创建与之前完全相同的可视化(条形图、散点图、直方图、箱形图和小提琴图),以便对所有包进行 1:1 的比较。我假设用于示例的数据已经准备好,分别为 DataFrames countriessubregions_cumregions_cum(如前所述)。

AlgebraOfGraphics

AoG 包可能是到目前为止对 GoG 的最纯粹的实现,正如我们在接下来的示例中将看到的。它建立在坚实的数学概念之上,作者将其描述为“一种用于数据可视化的声明式、问题驱动的语言”。其主要开发者是 Pietro Vertechi

在技术层面上,它采用了与我们到目前为止看到的包完全不同的方法:Gadfly.jl 是一个完全用 Julia 编写的独立图形包,而 VegaLite.jl 是一个用于 Vega-Lite 图形引擎的 Julia 接口,AoG 则是 [Makie.jl](https://docs.makie.org/stable/) 的附加包。Makie 本身是 Julia 生态系统中最年轻的图形包(也是完全用 Julia 编写的)。

AoGMakie 之间的界限是流动的。AoG 的几个元素使用了 Makie 的属性,并且如果某些方面无法使用 AoG 本身的概念来表达,Makie 总是备用解决方案。

还应注意的是,AoG 仍然在进行中。0.1 版本仅于 2020 年发布。因此,它不如其他更成熟的包完整,某些方面尚未完全实现。

条形图

所以让我们进入第一个可视化图,它分别使用条形图描绘地区(即大陆)和子区域的人口规模。

按地区划分的人口

首先,我们想展示每个地区(即大陆)在 2019 年的人口规模,作为条形图中的一条条形。除此之外,每个“地区条形”应具有不同的颜色。

使用这个简单的例子,我们可以深入了解 AoG 的基本概念:用 GoG 术语来说,这个可视化基于 regions_cum 数据框的数据显示,并包含:

  • 将数据属性 Region 映射到 x 轴

  • 将数据属性 Pop2019 映射到 y 轴

  • 将数据属性 Region 映射到颜色

  • 使用“条形”几何体

正如我在 GoG 介绍中所解释的,其理念之一是可以从不同的构建块创建可视化规范,这些块可以根据特定需求组合。AoG 完全实现了这一理念。因此,我们可以直接将 GoG 描述转换为 AoG 元素:

  • regionPop2xy = mapping(:Region, :Pop2019) 是将 Region 映射到 x 轴,Pop2019 映射到 y 轴

  • region2color = mapping(color = :Region) 是将 Region 映射到颜色的映射

  • barplot = visual(BarPlot) 是“条形”几何体

现在我们可以组合这些构建块(使用操作符 *),从 regions_cum 中获取数据,并通过调用 draw 创建图表:

draw(data(regions_cum) * regionPop2xy * region2color * barplot)

这会生成以下条形图:

按地区划分的人口(1) [图片由作者提供]

与之前的文章一样,我们还为每个可视化创建了美化版本,添加了标签、标题和漂亮的背景色等。这可以在 AoG 中使用 Makie 参数 axisfiguredraw 实现:

这将生成以下图表:

按地区划分的人口(2) [图片由作者提供]

按子区域划分的人口

现在,让我们继续可视化按 子区域 划分的人口。这基本上与上述图表相同,但我们使用 subregions_cum 中的数据,而不是 regions_cum

所以我们现在的轴映射是 subregionPop2xy = mapping(:Subregion, :Pop2019)。由于我们希望子区域的条形图按地区着色,我们可以重复使用上面的映射,基本图表可以通过以下方式绘制:

draw(data(subregions_cum) * subregionPop2xy * region2color * barplot)

这将生成以下图表:

按人口划分的子区域(1) [图片由作者提供]

显然,如果我们选择水平条形图,子区域标签会更易读。这可以通过交换映射到轴的数据属性实现:subregionPop2xy_hor = mapping(:Pop2019, :Subregion) 并在 visual 中添加 orientation = :x。绘制水平版本的条形图的代码是:

draw(data(subregions_cum) * subregionPop2xy_hor * region2color * 
     visual(BarPlot; direction = :x))

不幸的是,这是一个说明AoG仍在进展中的规格。在渲染过程中一定有一些 bug,因为这个draw命令的结果如下所示:

按人口划分的子区域(2)[图片来源:作者]

y 轴上的刻度以及条形图的位置不正确,x 轴上的刻度也不是我们想要的。

使用 Makie.jl 按子区域划分的人口

因此,我们把这个问题作为一个机会来切换到Makie.jlMakie是一个相当低级的图形包。在我们之前看到的包中自动获得的许多功能,在Makie中需要明确指定。这给程序员提供了很多控制权,但使得规格描述非常冗长。

另一个缺点是,Makie无法处理名义数据。所有名义数据在可视化之前必须转换为数字形式。在我们的例子中,这意味着我们需要将RegionSubregion属性的名义数据转换为数字:

  • 对于Subregion来说,这相对简单,因为这个属性包含唯一值。因此,我们只需使用该数据帧列的索引值,并将其存储在新列subregion_num中。

  • Region值不是唯一的。因此,我们首先将其转换为CategoricalArray,这会隐式地映射到数值。然后,我们可以使用函数levelcode获得相应的数字,并将其存储在另一个新列region_num中。

除此之外,我们从ColorSchemes.jl中选择了一个合适的颜色方案(Set2_8),以获得美观且易于区分的区域颜色。这个方案如下所示:

颜色方案 Set2_8 [图片来源:作者]

为了进行这些准备,我们需要以下代码:

然后我们将直接创建一个“美化”版的条形图,带有标签等。在Makie中,我们需要一个Figure作为基础元素,barplot可以放置在其中。由于Makie无法处理名义数据,我们还需要使用yticks属性手动指定 y 轴的刻度,如下代码所示,这样可以创建我们的水平条形图:

这段代码比较长,但结果看起来相当令人满意:

按子区域划分的人口(3)[图片来源:作者]

为了获得一个按人口规模排序的条形图版本,我们需要使用sort!(subregions_cum, :Pop2019)相应地对subregions_cum中的数据进行排序,然后重新执行上面的代码(包括映射到数字数据)。这将生成如下图表:

按子区域划分的人口(4)[图片来源:作者]

散点图

在这次Makie的插曲之后,我们回到AoG,尝试可视化人口变化如何依赖于人口规模。我们可以使用如下的散点图来实现:

popChangeVsPop = data(countries) * 
      mapping(:Pop2019, :PopChangePct) * 
      mapping(color = :Region)
draw(popChangeVsPop)

规格包括将Pop2019映射到 x 轴,将PopChangePct映射到 y 轴,以及将Region映射到颜色(我们本可以在此时重用region2color,但也可以直接指定映射)。此处可以省略visual,因为在这种情况下,AoG默认使用点几何图形(Scatter)。这给我们提供了以下图表:

人口相关的增长率(1) [作者提供的图片]

与之前的文章一样,我们现在通过在 x 轴上使用对数刻度来改进可视化,因为数据相当偏斜。此外,我们通过添加标签、标题等来进行“美化”。所有这些可以通过重用图表规格popChangeVsPop并通过传递适当的参数给draw来实现:

这导致了以下图表:

人口相关的增长率(2) [作者提供的图片]

直方图

现在我们切换到直方图,用于描述不同国家之间的人均 GDP 分布。由于AoG提供了所谓的histogram-analysis,规格非常简单:

draw(data(countries) * mapping(:GDPperCapita) * histogram())

分析AoG中是可视化之前处理数据的一种方式。通常几何图形(visual)直接依赖于分析,例如在这个例子中,直方图将自动使用条形几何图形进行显示。

人均 GDP 的分布(1) [作者提供的图片]

直方图的创建可以通过改变箱数(通过参数bins)和使用不同的normalization算法来影响。因此,我们通过以下规格得到了改进版本:

这段代码再次展示了AoG如何将可视化的规格(histGDPperCapita)与其“美化”(在调用draw时)分离开来,从而生成以下图表:

人均 GDP 的分布(2) [作者提供的图片]

箱线图和小提琴图

最后,我们使用箱线图和小提琴图可视化每个地区的人均 GDP 分布。这可以像之前一样简单实现,因为AoG为这两种图表变体提供了特定的几何图形。

为了最大化元素的重用,我们首先定义数据和分布的映射(distGDPperCapita),然后添加几何图形(使用visual)。如所有示例中所示,可以通过在调用draw时传入适当的参数来添加额外的“美化”。

这段代码创建了以下两个图表:

按地区划分的人均 GDP 分布(1) [作者提供的图片]

按地区划分的人均 GDP 分布(2) [作者提供的图片]

放大

由于两个图中“最有趣”的部分都位于 0 到 100,000 的范围内(在 y 轴上),我们希望将图表限制在该范围内(进行某种缩放)。

AoG中,这可以通过datalimits参数应用于visual来实现。但似乎AoG中存在另一个 bug,因为这个参数只有在用于小提琴图时才有期望的效果,而在应用于箱线图时则没有任何变化。

因此,使用以下规格 …

violinRestricted = distGDPperCapita * 
                   visual(Violin; show_notch = true, datalimits = (0, 100000))
drawDist(violinRestricted)

… 我们得到这个图表:

各地区人均 GDP 分布 (3) [图片由作者提供]

结论

如上所述,AoG包显然是我们在本系列中看到的最纯粹的图形语法实现。它确实将映射、几何等分离成不同的构建模块,然后可以使用*运算符组合起来。它还清晰地将更多“装饰性”元素(所有我们上面称之为“美化”的东西)与实际可视化分开,从而使规格更加模块化,并提供了更多可以重用的构建块。

我认为,对于这样一个年轻的包来说,仍有一些粗糙之处是很正常的,但它确实有坚实的基础,看起来相当有前景。当然,在这篇文章中无法展示AoG的所有功能。因此,如果你想了解更多,请查看文档。最后但同样重要的是,了解这种方法的基本理念也是值得的,可以在这里找到。

对于那些希望深入研究代码的人,还有一个Pluto notebook,其中包含我在 GitHub 库中展示的所有示例。

统计学 Bootcamp 8:两个均值的故事

原文:towardsdatascience.com/statistics-bootcamp-8-a-tale-of-two-means-ecec013ae414

统计学 Bootcamp

学习你作为数据科学家每日使用的库背后的数学和方法

Adrienne KlineTowards Data Science Adrienne Kline

·发布于 Towards Data Science ·13 分钟阅读·2023 年 1 月 4 日

--

作者提供的图片

本文是一个更大 Bootcamp 系列的一部分(请参见 kicker 以获取完整列表!)。这一部分致力于学习如何比较两个总体,基于我们对样本均值与总体均值的了解。

我们可能想调查两个总体(或两个样本)在某些方面是否存在差异。例如,在西北大学或芝加哥大学医学院就读的人在年龄上是否存在差异?在这里,我们寻求比较两个均值,而不是在之前的 Bootcamp 中,我们尝试查看样本均值与总体均值之间是否存在差异。

我们将关于年龄和医学生的问题概述如下:

  1. population 1 = 所有在 UIC 注册的医学生

    population 1 = 所有在西北大学注册的医学生

  2. 假设(在双尾检验的情况下):

比较两个总体

如果我们想比较两个总体,基于 我们在之前的 Bootcamp 中学到的知识,我们回到了我们钟爱的 z 检验。我们对这个检验的假设是总体彼此独立,两者的标准差已知,样本要么服从正态分布,要么样本量≥30,并且样本是随机的。

我们可以将假设比较表述如下:

对于两个独立的总体,它们各自的均值分别为 xbar_1 和 xbar_2,相关的方差为 σ² 和 σ²₂。

平均差异:

方差:

标准差:

两个总体均值差异的置信区间

扩展计算单变量的置信区间(CIs)(训练营 6),我们取均值的差异(估计值),并将每个总体的标准误差相加:

示例。 一个 USC Trojans 橄榄球队样本的平均体重为 120 公斤,总体方差为 49 公斤。100 名 Notre Dame 的 fighting Irish 的平均体重为 99 公斤,总体方差为 50 公斤。计算均值差异的 95 百分位置信区间。

两样本 z 检验

我们可以使用两样本 z 检验来检验两个总体均值的假设。我们的假设包括:1) 总体彼此独立,2) 总体是大的或正态分布的,3) 简单随机样本(SRS),4) 每个总体的标准差已知。为此,我们将按照以下步骤进行:

  1. 写出我们的假设陈述,比较均值。H₀: μ1 = μ2 其中我们的备择假设 H₁可以是:μ1 ≠ μ2(双尾),μ1 < μ2(左尾)或μ1 > μ2(右尾)。

  2. 设定显著性水平,alpha。

  3. 计算 z 统计量:

为了对这个方程有一些直观的理解,可以将分子(x_1 — x_2)视为观察到的总体差异,而期望的(μ_1 — μ_2)我们用 0 来表示,即没有差异(零假设)。

例如。假设我们正在决定是否去迪士尼乐园还是迪士尼世界度假。我们正在跟踪每个地方的机票价格以决定去哪儿。现在有促销,你可以以 88.42 美元的价格购买到迪士尼乐园的往返票,而迪士尼世界的票价为 80.61 美元。假设这些数字是从 50 个航班选项的两个样本中生成的,标准差分别为 5.62 美元和 4.83 美元。在α = 0.05 时,你能得出存在成本差异的结论吗?

  1. 设定假设:H₀: μ1 = μ2 H₁: μ1 ≠ μ2

  2. α = 0.05

  3. 对于α = 0.05,我们的对应临界值是± 1.96

  4. 我们计算得到的 z 分数是:

5. 比较计算得到的 z 值与 z 临界值:7.45 > 1.96

6. 根据比较结果,我们会拒绝零假设

7. 解释我们的发现。有足够的证据表明均值不相等,并且迪士尼乐园与迪士尼世界之间的比率存在差异。

两样本 t 检验

到目前为止,你已经了解到我们可以使用 z 检验来调查两个总体均值之间的差异,当每个总体的方差已知时。如果我们没有关于总体方差的信息,我们会返回使用 t 检验(也称为学生 t 检验)来比较两个均值。

t 检验分为两类:非配对配对。当你看到配对与非配对时,在处理总体时将其等同于 独立依赖。配对检验用于考察同一总体在变化前后的情况,或在随机对照试验(RCTs)中配对样本。独立总体是指它们在某个方面被检验,但群体在其他方面彼此独立。非配对 t 检验(简称 t 检验)可以进一步分为两类:合并 t 检验非合并 t 检验

合并 t 检验 用于总体方差相等或假设相等的情况。非合并 t 检验 用于方差不等或假设不等的情况。我们可以通过较大(s)和较小(s)标准差的平方比来进行(非正式)检查。

如果:

sₗ² / sₛ² < 4 或 sₗ / s < 2:使用合并 t 检验

否则:使用非合并 t 检验

合并 t 检验

在进行合并 t 检验以比较两个总体均值时,我们会有通常的假设:简单随机样本,正常/大样本量,总体/样本独立。但我们还将添加假设,即总体的标准差相等。

  1. 陈述我们的备择假设和原假设:H₀(μ₁ = μ₂)和 H₁

  2. 根据我们的假设确定检验是右尾(μ₁ > μ₂)、左尾(μ₁ < μ₂)还是双尾检验(μ₁ ≠ μ₂)

  3. 确定显著性水平 α

  4. 计算检验统计量:

其中 sp 为:

并且 n1n2 是每个样本中的实体数量。注意 sp 是一个“合并”标准差,这使得该检验成为合并 t 检验。计算 sp 时,我们根据各自的样本量创建了 σ (sp) 的加权估计。我们可以使用临界值或 p 值方法继续:

5. 临界值为:±t_{α/2}(双尾),-t_{α}(左尾)或 t_{α}(右尾),其中 DOF = n1 + n2–2 = (n1–1) + (n2–1)。可以通过 t 表确定这些值。同样的 t 表也可以估算 p 值。

6. 如果检验统计量落入拒绝区域或 p 值 ≤ α,我们拒绝 H₀,否则我们不能拒绝 H₀。

7. 解释和总结统计检验结果。始终注意,假设检验对于正态总体是精确的,对于非正态但大的总体是近似的。

示例。 一名足球教练想确定电解质液体的消耗是否在他的足球队和田径校队之间有所不同,以升/周(L/week)为单位。随机选取了每队的 10 名运动员。

track:
1.65
3.24
1.87
1.64
2.35
3.39
0.56
1.93
2.21
football:
2.23
2.78
1.89
3.67
1.45
1.28
3.57
2.63
2.78
  1. H₀: μ₁ = μ₂ 和 H₁: μ₁ ≠ μ₂(双尾),总体 1:田径,总体 2:足球

  2. α = 0.05

  3. 样本均值和标准差:

4. 合并方差 sp:

5. 检验统计量:

6. 自由度(DOF):10 + 10 -2 = 18,tcrit:-2.101。由于 -1.89 不小于 -2.101,我们无法拒绝 H0。

7. 由于没有足够的证据表明两个人群之间的电解质液体消费存在差异,我们无法拒绝 H₀。

σ 相等时的两个均值的置信区间(CI)

当计算两个 独立 样本均值的置信区间且 σ 相等时的假设包括:正态/大样本,均等的总体 σ(标准差),简单随机抽样(SRS)和独立总体。

  1. 根据 α 选择置信水平,CI = 1 - α

  2. 确定自由度 = n1 + n2 — 2

  3. 使用 t 表格,利用自由度(DOF)查找 ±t_{α/2}

  4. 置信区间的范围由公式确定,形式为(下限,上限):

5. 现在我们可以根据置信区间(CI)进行解释和建议。

非合并 t 检验

非合并 t 检验与合并 t 检验几乎相同。然而,它的不同之处在于假设总体的标准差是 不等的。这最终导致了检验统计量 t 和自由度(DOF)的具体计算差异——通常用 Δ 表示。如果返回浮点值,我们需要向下取整到最近的整数。我们按照上面合并 t 检验中的步骤 1–3,因此我们将跳到步骤 4:

4. 确定自由度(Δ):

5. 确定 t 统计量:

6. 使用 t 表格将 t 统计量与临界值或 p 值进行比较,如假设检验所示:±t_{α/2}(双尾),-t_{α}(左尾)或 t_{α}(右尾)

7. 如果 P ≤ α 或检验统计量落在拒绝区域,拒绝 H0,否则无法拒绝 H0。我们现在可以解释我们的结果,并根据数据提出建议。

当 σ 不相等时的两个均值的置信区间(CI)

当计算两个 独立 样本均值的置信区间且 σ 不相等时的假设包括:正态/大样本,不等的总体 σ(标准差),简单随机抽样(SRS)和独立总体。

  1. 根据 α 选择置信水平,CI = 1 - α

  2. 确定自由度(记得向下取整到最近的整数):

3. 使用 t 表格,利用自由度(DOF)查找 ±t_{α/2}

4. 置信区间的范围由公式确定,形式为(下限,上限):

5. 现在我们可以根据置信区间(CI)进行解释和建议。

示例。 一名高中田径教练想知道短跑运动员的鞋钉是否能改善他们的平均时间(减少)。在显著性水平为 0.01 的情况下,有足够的证据得出结论吗?使用鞋钉的运动员与不使用鞋钉的运动员相比,其 100 米平均时间是否减少了?

spikes
11.2
12.1
10.9
11.2
12.1
10.9
11.8
12.1
10.9
11.7
12.5
10.9
12.1
10.6
11.1

no spikes
13.6
14.1
12.3
11.5
15.8
13.6
14.1
12.3
10.5
15.8
13.6
16.2
12.7
10.8
15.8
  1. 计算每个样本的均值和标准差:

2. s_l/s_s = ~3,这大于 2,因此我们可以假设方差不相等,然后进行非合并 t 检验。

3. H₀:μ₁ = μ₂ 和 H₁:μ₁ < μ₂(左尾),种群 1:有鞋钉,种群 2:无鞋钉

4. α = 0.01

5. 计算 t 统计量:

6. 自由度:

7. t_{df,α} 的临界值 = t_{18,0.01} = 2.552。我们需要将其转换为负值(-2.552),因为标准 t 表不总是提供方向性。与我们计算出的 -4.20 相比,我们可以看到它落在我们假设检验的拒绝区域。

8. 我们拒绝 H₀,并得出结论:样本数据中有足够的证据表明,使用鞋钉的 100 米短跑运动员的时间减少了。

配对样本检验

在配对假设检验中,我们希望控制个体差异对结果测量的影响。这通常表现为对个体进行结果测量,实施干预,然后再次对个体进行结果测量。不同的是,我们将其扩展到许多人,并进行汇总,以查看是否存在总体效益。例如,学生在睡眠 5 小时与 8 小时后在数学测试中的表现是否有所不同?将其概括,我们称之为重复测量匹配设计。随机对照试验(RCTs)尝试做同样的事情,只是他们必须找到与接受治疗(或没有治疗)的人相似的患者。这两个例子不再代表两个独立的样本

配对 t 检验

配对 t 检验用于比较配对样本中的两个均值。假设与我们的独立 t 检验相同——样本是随机获得的,并且要么是大样本,要么是正态分布的。然而,我们不再假设样本彼此独立

我们的假设如下:简单随机样本(SRS)和正态/大样本。

  1. 陈述我们的备择假设和原假设:H₀(μ₁ = μ₂)和 H₁

  2. 根据我们的假设,确定检验是否为右尾(μ₁ > μ₂)、左尾(μ₁ < μ₂)或双尾检验(μ₁ ≠ μ₂)

  3. 确定显著性水平 α

  4. 计算样本中每个实例的差异(例如,前后对比)

  5. 计算检验统计量:

其中 d_bar 是差异的均值,s_d 是差异的标准差,n 是样本量。

5. 临界值为:±t_{α/2}(双尾),-t_{α}(左尾)或 t_{α}(右尾),自由度 DOF = n-1。使用 t 表可以确定这些值。相同的 t 表也可以估算 p 值。

6. 如果检验统计量落在拒绝区域内或 p 值 ≤ α,我们拒绝 H₀,否则我们不能拒绝 H₀。

7. 解释和总结统计检验的结果。始终注意,假设检验对于正态分布的人群是准确的,对于非正态但样本量大的群体则是近似的。

例如。一位医生声称某种药物能帮助超重患者减轻体重。一个样本中 10 名患者在开始治疗前和 1 个月后称重。这些数据是否支持医生在 p 值为 0.05 时的声明?

  1. 陈述假设:H₀: μ2 = μ1 H₁: μ2 < μ1。这构成了一个左侧检验

  2. alpha = 0.05

  3. 确定配对数据的平均差异(治疗后 - 治疗前)

4. 确定配对差异的标准差(此处 n=10)

5. 计算 t 统计量,我们得到:

6. 使用我们的 t 表,我们需要根据我们的自由度(DOF = n-1)和 alpha = 0.05 确定临界值。在这里,我们的 DOF = 10–1 = 9。t_crit = -1.833

7. 因为 -3.06 < -1.833,我们拒绝 H₀

8. 我们的结果是我们有足够的证据支持该减重药物导致统计学上显著的净体重减少的声明。

配对 t 区间计算

如果我们希望确定配对样本均值的置信区间(CI),µ1 和 µ2。请记住,对于差异不正态分布的大样本,置信区间计算只是近似的。我们计算 t 区间的假设是样本大/差异正态分布且样本是简单随机样本(SRS)。

  1. 确定置信水平 alpha

  2. 确定 DOF=n-1

  3. 使用 t 表查找 t_{α/2}

  4. µ1 — µ2 的置信区间边界计算为:

5. 我们现在可以解释我们的置信区间,并从中得出结论。

总结

在这次训练营中,我们详细讲解了 t 检验。具体来说,我们讨论了何时使用配对 t 检验和非配对 t 检验。阅读本文后,你应该能够识别数据违反独立性假设的情景,并且对于那些保持独立的数据,如何进行必要的计算。我们还探讨了独立 t 检验中的子类别——即合并和非合并检验,因此请始终记得检查你的标准差。每当我们从数据中进行推断时,始终重要的是承认我们在进行分析时所做的假设。

除非另有说明,所有图片均由作者创作。

此外,如果你喜欢看到类似的文章,并且希望无限制访问我的文章以及 Medium 提供的所有文章,请考虑使用下面的推荐链接进行注册。会员费用为每月 5 美元(USD);我会获得一小部分佣金,这反过来有助于提供更多的内容和文章!

[## 使用我的推荐链接加入 Medium — Adrienne Kline

阅读 Adrienne Kline(以及 Medium 上的成千上万其他作家的)每一篇文章。你的会员费用将直接支持…

medium.com](https://medium.com/@askline1/membership?source=post_page-----ecec013ae414--------------------------------)

稳定航向:导航 LLM 应用程序评估

原文:towardsdatascience.com/steady-the-course-navigating-the-evaluation-of-llm-based-applications-8b7a22734fc9?source=collection_archive---------1-----------------------#2023-11-09

为何评估 LLM 应用程序至关重要及如何入门

Stijn GoossensTowards Data Science 斯坦·古森斯

·

关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 11 月 9 日

--

一位膝盖受伤的海盗向他基于 LLM 的急救助手寻求建议。图像由作者使用 DALL·E 3 生成。

简介

大型语言模型(LLMs)引起了广泛关注,许多人将其融入到他们的应用程序中。例如,回答关系数据库问题的聊天机器人、帮助程序员更高效编写代码的助手,以及代表你采取行动的副驾驶。LLMs 的强大能力使你能够快速取得项目的初步成功。然而,随着你从原型过渡到成熟的 LLM 应用程序,一个强大的评估框架变得至关重要。这样的评估框架帮助你的 LLM 应用程序达到最佳性能,并确保一致和可靠的结果。在本博客文章中,我们将讨论:

  1. 评估 LLM 与基于 LLM 的应用程序之间的差异

  2. LLM 应用评估的重要性

  3. LLM 应用评估的挑战

  4. 入门

    a. 收集数据和构建测试集

    b. 绩效衡量

  5. LLM 应用评估框架

通过虚构的例子“FirstAidMatey”,一个为海盗提供急救的助手,我们将深入探讨评估技术、挑战和策略的海洋。最后我们将总结关键要点和洞察。让我们开始这段启发性的旅程吧!

评估 LLM 与基于 LLM 的应用程序

对 OpenAI 的 GPT-4、Google 的 PaLM 2 和 Anthropic 的 Claude 等个体大型语言模型(LLMs)的评估通常通过基准测试如 MMLU 来进行。然而,在本博客文章中,我们感兴趣的是评估基于 LLM 的应用程序。这些应用程序由 LLM 提供支持,并包含其他组件,如管理 LLM 调用序列的编排框架。通常,检索增强生成(RAG)用于向 LLM 提供上下文,避免幻觉。简而言之,RAG 需要将上下文文档嵌入到向量存储中,以便从中检索相关片段并与 LLM 共享。与 LLM 相比,基于 LLM 的应用程序(或 LLM 应用)旨在非常好地执行一个或多个特定任务。找到合适的设置通常涉及一些实验和迭代改进。例如,RAG 可以通过多种方式实现。本文讨论的评估框架可以帮助你找到适合你用例的最佳设置。

在基于 LLM 的应用程序中使用 LLM 可以使其更强大。

FirstAidMatey 是一个基于 LLM 的应用,帮助海盗处理像“我的手被绳子缠住,现在肿了,我该怎么办,伙计?”这样的问题。最简单的形式是 Orchestrator 由一个单一的提示组成,将用户问题传递给 LLM,并要求其提供有帮助的回答。它还可以指示 LLM 用海盗语言回答,以便更好地理解。作为扩展,可以添加一个包含急救文档的向量存储。根据用户问题,可以检索相关文档并将其包含在提示中,以便 LLM 可以提供更准确的答案。

LLM 应用评估的重要性

在深入了解如何评估之前,让我们先看看为什么你应该建立一个评估 LLM 应用的系统。主要目标有三点:

  • 一致性:确保 LLM 应用在所有场景下的输出稳定可靠,并在出现回归时发现。例如,当你在特定场景下改善 LLM 应用表现时,你希望在可能影响其他场景的性能时收到警告。当使用像 OpenAI 的 GPT-4 这样的专有模型时,你还需遵循其更新计划。随着新版本的发布,你当前的版本可能会逐渐被弃用。研究表明,切换到更新的 GPT 版本并不总是更好。因此,能够评估新版本如何影响 LLM 应用的表现是很重要的。

  • 洞察:了解 LLM 应用表现良好的地方和需要改进的地方。

  • 基准测试:建立 LLM 应用的性能标准,衡量实验的效果,并自信地发布新版本。

结果是,你将实现以下成果:

  • 赢得 用户信任和满意度,因为你的 LLM 应用将表现一致。

  • 提升利益相关者信心,因为你可以展示 LLM 应用的表现如何,以及新版本如何改进旧版本。

  • 提升你的竞争优势,因为你可以快速迭代、进行改进并自信地部署新版本。

LLM 应用评估的挑战

阅读了上述好处后,很明显为什么采用基于 LLM 的应用可能是有利的。但在此之前,我们必须解决以下两个主要挑战

  • 缺乏标记数据:与传统机器学习应用不同,基于 LLM 的应用不需要标记数据即可开始。LLM 可以开箱即用地执行许多任务(如文本分类、摘要、生成等),无需展示具体示例。这很棒,因为我们不必等待数据和标签,但另一方面,这也意味着我们没有数据来检查应用的表现。

  • 多个有效答案:在 LLM 应用中,相同的输入通常可以有多个正确答案。例如,聊天机器人可能会提供多个意义相近的回答,或者代码可能会生成具有相同功能但结构不同的版本。

为了应对这些挑战,我们必须定义合适的数据和指标。我们将在下一节中进行说明。

开始

收集数据并构建测试集

对于评估基于 LLM 的应用,我们使用一个由每个包含特定输入和目标的测试用例组成的测试集。这些测试用例的内容取决于应用的目的。例如,代码生成应用期望将口头指令作为输入,并返回代码。评估期间,将把输入提供给 LLM 应用,生成的输出可以与参考目标进行比较。以下是 FirstAidMatey 的一些测试用例:

一些用于我们急救聊天机器人应用的测试用例。

在这个例子中,每个测试用例包含一个问题作为输入和一个正确的参考答案作为目标。除此之外,历史响应,甚至历史用户反馈也可以添加到测试用例中。这允许你检查较新的回答是否比旧的回答有所改进,以及用户反馈是否得到处理。理想情况下,你的测试集应包括经过验证的参考答案,但如果你希望快速比较新旧模型,使用历史响应和反馈也是一个选项。

由于你通常在没有数据集的情况下开始开发基于 LLM 的应用,因此早期开始构建测试集非常重要。你可以通过添加当前模型失败的那些示例来进行迭代。最初,将是开发人员最频繁地尝试 LLM 应用,并且是他们根据手动测试添加第一个测试用例。然而,尽早涉及业务或最终用户也很重要,因为他们对相关测试用例有更好的理解。为了启动测试集,你也可以使用 LLM 根据你的知识库等生成输入-目标对。随着时间的推移,测试集应该涵盖对最终用户最重要的所有主题。因此,收集用户反馈并为表现不佳和被低估的主题添加测试用例是很重要的。

评估性能

给定一组测试用例后,我们现在可以将输入传递给 LLM 应用,并将生成的响应与目标进行比较。由于每个输入没有单一的正确答案,我们不能字面上地将响应与参考答案进行比较。响应的措辞可能不同,但意义与参考答案相同。不过,我们可以做的是 评估响应的几个属性,作为响应质量的代理。测试的相关属性取决于应用程序和可用数据。以下是 FirstAidMatey 响应的属性列表,以及如何将其转化为指标:

  • 事实一致性:使用 LLM 来评估生成的答案是否与参考答案在事实上一致。

  • 海盗性:使用 LLM 来评估答案是否用海盗语言书写。

  • 语义相似性:计算生成答案与参考答案的嵌入之间的余弦相似度。请注意,这比事实一致性计算要便宜得多,但可能与正确答案的相关性不那么高。

  • 冗长性:将生成答案的长度除以参考答案的长度。冗长性越高,LLM 应用的表现越啰嗦,这可能不是应用的意图。

  • 延迟:测量 LLM 应用生成响应所需的时间。这使你能够在更准确但较慢的配置之间做出权衡。

根据使用案例,你可能会有更多/不同的属性。例如,对于一个代码生成的 LLM 应用,一个额外的属性可能是生成代码的语法正确性,这可以通过将代码发送给编译器来测量。下图说明了如何通过使用 LLM 来评估一个属性。

通过另一个 LLM 调用评估事实一致性的示意图。

一旦所有测试的属性都被评估完成,可以计算出每个指标的平均分数并与基准/目标性能进行比较。当你尝试不同的配置时,例如 RAG,测试分数会指向最有利的候选项。正确性、冗长性和延迟之间的权衡也会变得清晰。此外,除了指标分数,失败的案例还可以提供有用的改进见解。仔细查看一些失败的测试,它们可能会为进一步改善你的 LLM 应用提供有用的见解。

你可能会想,“如果 LLM 不能正确回答问题,我们怎么能相信它来评估这个答案?难道不会对它自己的输出有偏见吗?” 的确,这听起来有些违背直觉,但这里的主要观点是评估比生成更容易。可以考虑创作与评估画作的类比。当创作一幅画时,你需要同时考虑多个属性,如构图、颜色、视角、光影、质感、预期信息等。而评估一幅画则可以逐一关注每个属性;评价一幅完成的作品比从头开始创作一幅画要简单。LLM 也是一样:尽管生成一个恰当的响应可能具有挑战性,但批判性地评估现有的响应则更加直接。如果有参考响应,比如对于事实一致性属性,评估会变得更可行。

LLM 应用评估框架

让我们在最后一部分中将所有内容结合起来。下图展示了评估框架的概述,并说明了一个重要的反馈循环:

  1. LLM 应用、属性和测试用例会传递给评估器,评估器会循环遍历所有测试用例,并将测试输入传递给 LLM 应用。然后,通过遍历属性并收集结果作为指标来评估生成的输出。

  2. 评估结果被存储以供进一步分析。除了指标外,跟踪 LLM 应用的配置(即使用的 LLM 和参数、使用的 RAG 组件和参数、系统提示等)也很重要,这样你可以轻松区分最有前景的实验,并获得进一步改进应用的洞见。你可以将评估结果和 LLM 应用配置存储在自己的数据库中,或者使用像MLflow这样的工具,它能让你立即访问用户友好的界面。

  3. 一旦你对性能感到满意,你可以发布新版本的应用,无论是面向内部用户还是外部用户。

  4. 在项目的早期阶段,将由开发者进行测试和收集反馈。随后,可以从最终用户那里收集反馈,无论是直接的(如点赞/点踩和书面反馈)还是间接的(如对话回合、会话时长、接受的代码建议等)。

  5. 通过分析收到的反馈来扩展测试集,并添加测试用例以涵盖当前模型处理不充分的情况。

  6. 在收到的反馈中发现趋势,并将其转化为LLM 应用改进。根据情况,你可以改进协调器(例如,创建一个单独 LLM 调用的链而不是一个单一提示)、检索过程(例如,改进嵌入)或 LLM(例如,改变模型、参数或考虑微调)。

LLM 基础应用的评估框架概述。

关于步骤 1 和 2 的示例,请查看以下 Jupyter notebook。该 notebook 说明了本文博客中解释的概念。你可以看到如何定义和发送属性、测试用例以及 LLM 应用程序版本给 Evaluator。除了打印评估结果外,这些结果还会被记录到 MLflow 仪表板中以便进一步分析。绝对值得一看,这将使讨论的话题更加具体。

结论

对 LLM 基础应用进行评估是 LLM 应用程序开发的重要部分。本文介绍的评估框架简化了开发过程中实验的比较,确保了一致的性能,并提供了进一步改进的见解。

我们遇到的第一个挑战是缺乏标记数据,可以通过尽早构建测试集并通过添加困难和代表性不足的案例来逐步扩展解决。第二个挑战是存在多个正确答案这一事实,可以通过查看生成输出的不同属性来克服。其中一些属性可以通过简单的公式或规则来衡量,而其他属性则可以通过 LLM 来评估。

最后,我们发现了一个关键的反馈循环,即评估结果和收集的用户反馈推动 LLM 应用程序的性能提升。总之,系统化的评估是指引你的 LLM 应用程序走向成功的指南针!

作者注:本文的灵感源于我在 Radix 担任解决方案架构师的经验。Radix 是一家比利时人工智能公司,专注于开发创新的机器学习解决方案和应用程序,服务于各种行业。如果你对 LLM 基础应用感兴趣,我鼓励你访问 radix.ai 或直接通过 LinkedIn与我联系。

除非另有说明,否则所有图片均为作者提供。

步骤基础:文本分类器

原文:towardsdatascience.com/step-by-step-basics-text-classifier-e666c6bac52b?source=collection_archive---------2-----------------------#2023-02-17

构建一个监督学习文本分类器的指导手册和流程图(Python)

Lucy DickinsonTowards Data Science Lucy Dickinson

·

关注 发表于 Towards Data Science ·10 分钟阅读·2023 年 2 月 17 日

--

图片由 Patrick Tomasso 提供,来源于 Unsplash

说到重点,构建文本分类器和理解自然语言处理 (NLP) 的世界涉及很多步骤。这些步骤必须按特定顺序实施。如果数据中的目标类不平衡,还需要更多步骤。从头学习这一切可能有些困难。尽管网上有许多学习资源,但找到一个全面的高水平指南却不易。因此,我写这篇文章,希望能通过一个 10 步简单指南来提供一些过程透明度。

我将从提供一个我整理的流程图开始,其中包含了从明确任务到部署训练好的文本分类器的所有必要步骤和关键点。

首先,什么是文本分类器?

文本分类器是一种算法,它通过学习单词的出现或模式来预测某种目标或结果,通常是一个类别,比如判断一封邮件是否是垃圾邮件。

在这里需要提到的是,我将专注于使用监督学习方法来构建文本分类器。另一种方法是使用深度学习方法,如神经网络。

让我们来看看那个流程图。

作者提供的图表

信息量很大。让我们将其分解成小块,逐步讲解每个部分。

1. 明确任务

这是任何数据科学项目中最重要的步骤之一。确保你完全理解所提出的问题。你是否有相关数据来回答这个问题?你的方法是否与利益相关者的期望一致?如果需要利益相关者的认可,不要去构建一个复杂难懂的模型。从简单的开始,让所有人都参与到这个过程中来。

2. 数据质量检查

任何项目中的另一个关键步骤。你的模型的好坏取决于输入的数据,因此确保删除重复数据并妥善处理缺失值。

3. 探索性数据分析 (EDA)

现在我们可以进入一些文本数据特定的分析。EDA(探索性数据分析)主要是理解数据,感受从中可以得出的信息。这个步骤的一个关键点是理解目标类分布。你可以使用 pandas 的 .value_counts() 方法或绘制条形图来可视化数据集中每个类的分布。你将能够看到哪些是主要类次要类

二分类标记数据集的不平衡类分布

模型在处理不平衡数据时表现不好。模型往往会忽略少数类别,因为数据量不足以训练模型检测这些类别。不过,如果你发现自己有一个高度倾斜的目标类别的不平衡数据集,这其实并不是世界末日。这在实际上是非常正常的。只要在模型构建过程开始之前知道这一点,以便在后续可以进行调整,就很重要。

不平衡数据集的存在也应该让你思考应使用哪些指标来评估模型性能。在这种情况下,‘准确率’(正确预测的比例)真的不是你的朋友。假设你有一个二分类目标类的数据集,其中 80%的数据标记为‘红色’,20%的数据标记为‘蓝色’。你的模型可以简单地对整个测试集预测‘红色’,并且仍然保持 80%的准确率。因此,模型的准确率可能具有误导性,因为你的模型可能只会预测多数类。

一些更好的指标包括召回率(正确预测的真正例比例)、精准率(正确预测的正例比例)或这两者的平均值,即F1 分数。在模型构建阶段,要特别关注这些指标,尤其是少数类的指标。你会希望提升这些得分。

4. 文本预处理

现在进入一些有趣的内容!文本数据中可能包含大量对任何机器学习模型都不实用的内容(这取决于任务的性质)。这个过程实际上是为了去除数据集中的“噪声”,使单词同质化,并将其简化到最基本的状态,以便仅保留有用的单词和最终的特征。

通常,你会希望去除标点符号、特殊字符、停用词(如‘this’,‘the’,‘and’),并将每个单词还原到其词根或词干。你可以尝试编写自己的函数,了解数据中的内容,然后进行清洗。例如,考虑下面的函数:

#  exploring patterns in the text to assess how best to cleanse the data
pat_list = [r'\d', '-', '\+', ':', '!', '\?', '\.', '\\n'] # list of special characters/punctuation to search for in data

def punc_search(df, col, pat):
    """
    function that counts the number of narratives
    that contain a pre-defined list of special
    characters and punctuation
    """
    for p in pat:
        v = df[col].str.contains(p).sum() # total n_rows that contain the pattern
        print(f'{p} special character is present in {v} entries')

punc_search(df, 'text', pat_list)

# the output will look something like this:

"""
\d special character is present in 12846 entries
- special character is present in 3141 entries
\+ special character is present in 71 entries
: special character is present in 1874 entries
! special character is present in 117 entries
\? special character is present in 53 entries
\. special character is present in 16962 entries
\n special character is present in 7567 entries
"""

当你对需要从数据中去除的内容有了更好的了解后,可以尝试编写一个函数,一次性完成所有操作:

lemmatizer = WordNetLemmatizer()  # initiating lemmatiser object

def text_cleanse(df, col):
    """
    cleanses text by removing special
    characters and lemmatizing each
    word
    """
    df[col] = df[col].str.lower()  # convert text to lowercase
    df[col] = df[col].str.replace(r'-','', regex=True) # replace hyphens with '' to join hyphenated words together
    df[col] = df[col].str.replace(r'\d','', regex=True) # replace numbers with ''
    df[col] = df[col].str.replace(r'\\n','', regex=True) # replace new line symbol with ''
    df[col] = df[col].str.replace(r'\W','', regex=True)  # remove special characters
    df[col] = df[col].str.replace(r'\s+[a-zA-Z]\s+',' ', regex=True) # remove single characters
    df[col] = df.apply(lambda x: nltk.word_tokenize(x[col]), axis=1) # tokenise text ready for lemmatisation
    df[col] = df[col].apply(lambda x:[lemmatizer.lemmatize(word, 'v') for word in x]) # lemmatise words, use 'v' argument to lemmatise versbs (e.g. turns past participle of a verb to present tense)
    df[col] = df[col].apply(lambda x : " ".join(x)) # de-tokenise text ready for vectorisation

然后你可以在清洗后的数据上再次运行第一个函数,以检查所有你想要去除的内容是否确实被去除了。

对于那些注意到上述函数没有去除任何停用词的人,观察得很好。在向量化过程中的几步操作中,你可以去除停用词。

5. 训练-测试拆分

这个步骤有自己的子标题,因为在开始调整特征之前执行这个步骤非常重要。使用 sklearn 的 train_test_split()函数拆分数据,然后保持测试数据不变,以避免数据泄漏的风险。

如果你的数据不平衡,有一些可选参数(‘shuffle’ 和 ‘stratify’),你可以在测试训练拆分时指定,以确保目标类别之间的均匀分配。这确保了你的少数类别不会全部集中在训练集或测试集中。

# create train and test data split
X_train, X_test, y_train, y_test = train_test_split(df['text'], # features
                                                    df['target'], # target
                                                    test_size=0.3, # 70% train 30% test
                                                    random_state=42, # ensures same split each time to allow repeatability
                                                    shuffle = True, # shuffles data prior to splitting
                                                    stratify = df['target']) # distribution of classes across train and test

6. 文本向量化

模型无法解释词语。相反,词语必须通过一种称为向量化的过程转换为数字。向量化有两种方法:词袋模型和词嵌入。词袋模型寻找文本之间词语的精确匹配,而词嵌入方法考虑词语的上下文,因此可以在文本之间寻找相似的词。比较这两种方法的有趣文章可以在这里找到。

对于词袋模型,句子会被分词,每个唯一的词成为一个特征。数据集中每个唯一的词将对应一个特征,每个特征会有一个整数值,这个整数值取决于该词在文本中出现的次数(词频向量 — sklearn 的 CountVectorizer())或者一个加权整数,表示该词在文本中的重要性(TF-IDF 向量 — sklearn 的 TfidVectorizer())。有关 TF-IDF 向量化的有用文章可以在这里找到。

确保在训练数据上训练向量化对象,然后使用它来转换测试数据。

7. 模型选择

尝试几种分类模型,看看哪一种在你的数据上表现最好。然后,你可以使用性能指标选择最合适的模型进行优化。我通过运行一个 for 循环,使用 cross_validate() 函数对每个模型进行迭代来完成这个任务。

#  defining models and associated parameters
models = [RandomForestClassifier(n_estimators = 100, max_depth=5, random_state=42), 
          LinearSVC(random_state=42),
          MultinomialNB(), 
          LogisticRegression(random_state=42)]

kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=1) # With StratifiedKFold, the folds are made by preserving the percentage of samples for each class.

scoring = ['accuracy', 'f1_macro', 'recall_macro', 'precision_macro']

#  iterative loop print metrics from each model
for model in tqdm(models):
    model_name = model.__class__.__name__
    result = cross_validate(model, X_train_vector, y_train, cv=kf, scoring=scoring)
    print("%s: Mean Accuracy = %.2f%%; Mean F1-macro = %.2f%%; Mean recall-macro = %.2f%%; Mean precision-macro = %.2f%%" 
          % (model_name, 
             result['test_accuracy'].mean()*100, 
             result['test_f1_macro'].mean()*100, 
             result['test_recall_macro'].mean()*100, 
             result['test_precision_macro'].mean()*100))

8. 基线模型

在你过于热衷于调整所选模型的超参数以提高性能指标之前,停下。记录下你模型的表现,然后再开始优化。只有通过与基线得分进行比较,你才能知道(并证明)你的模型是否有所改进。如果你需要向利益相关者展示你的方法,这也有助于你获得他们的认可和讲述故事。

创建一个空的 DataFrame,然后在每次模型迭代后,附加你选择的指标以及迭代的编号或名称,以便你可以清楚地看到模型在优化尝试中的进展。

9. 模型调整 — 纠正数据不平衡

通常,微调你的模型可能涉及调整其超参数和特征工程,以提高模型的预测能力。然而,在这一部分中,我将重点关注用于减少类别不平衡影响的技术。

除了为少数类收集更多数据外,还有 5 种方法(我所知道的)可以用来解决类别不平衡问题。大多数方法是一种特征工程,目的是通过过采样少数类或欠采样多数类来平衡整体类别分布。

我们来快速看一下每种方法:

  1. 添加少数类惩罚

分类算法有一个参数,通常称为‘class_weight’,你可以在训练模型时指定。这本质上是一个惩罚函数,如果少数类被错误分类,将给予更高的惩罚以防止误分类。你可以选择自动参数,也可以根据类别手动分配惩罚。请务必阅读你正在使用的算法的文档。

2. 过采样少数类

随机过采样涉及随机复制少数类样本,并将其添加到训练数据集中以创建均匀的类分布。此方法可能导致过拟合,因为没有生成新的数据点,因此请务必检查这一点。

python 库 imblearn 包含用于过采样和欠采样数据的函数。重要的是要知道,任何过采样或欠采样技术仅应用于训练数据。

如果你使用交叉验证方法来将数据拟合到模型中,你需要使用管道以确保仅对训练折进行过采样。Pipeline() 函数可以从 imblearn 库中导入。

over_pipe = Pipeline([('RandomOverSample', RandomOverSampler(random_state=42)), 
                      ('LinearSVC', LinearSVC(random_state=42))])

params = {"LinearSVC__C": [0.001, 0.01, 0.1, 1, 10, 100]}

svc_oversample_cv = GridSearchCV(over_pipe, 
                                 param_grid = params, 
                                 cv=kf, 
                                 scoring='f1_macro',
                                 return_train_score=True).fit(X_train_vector, y_train)
svc_oversample_cv.best_score_  # print f1 score

3. 欠采样多数类

与上述方法的另一种替代方案是对多数类进行欠采样,而不是对多数类进行过采样。有人可能会认为如果你拥有数据,就不值得删除数据,但这可能是一个值得尝试的选项。同样,imblearn 库中也有过采样函数可以使用。

4. 合成少数类的新实例

少数类的新实例可以通过称为 SMOTE(合成少数过采样技术)的方法生成,该方法同样可以使用 imblearn 库来实现。这里有一篇很好的文章 here 提供了一些实现 SMOTE 的示例。

5. 文本增强

可以使用现有数据的同义词生成新数据,以增加少数类的数据点数量。方法包括同义词替换和回译(翻译成一种语言再翻译回原始语言)。nlpaug library 是一个探索这些选项的方便库。

逐步执行这些平衡处理步骤,并将得分与基准得分进行比较,将允许你查看哪种方法最适合你的数据。

10. 部署训练好的分类器

现在是将经过训练的分类器推向生产环境的时候了,让它在未见过和未标记的数据上发挥魔力,前提是它已经经过测试。部署的方法取决于你的公司使用的平台,这里有一篇文章详细介绍了一些选项。

就这样!10 个简单步骤,使用监督式机器学习方法在 python 中构建文本分类器。总结一下,我们学习了:

  • 构建文本分类器所需的步骤顺序

  • 检查类别分布的重要性,并理解这如何影响模型性能指标

  • 文本预处理步骤

  • 如何选择合适的模型并记录基准模型的性能

  • 解决类别不平衡的方法

希望这对你有所帮助。请留下任何想法、评论或建议 😃

数据科学家的指南:将洞察力转化为有影响力的行动

原文:towardsdatascience.com/step-by-step-guide-the-art-of-winning-stakeholders-as-a-data-scientist-to-drive-impact-8dbb4e4a5179

数据科学家在科技行业的第一年编年史

将你的工作转化为有意义成果的杀手技能组合

Khouloud El AlamiTowards Data Science Khouloud El Alami

·发表于Towards Data Science ·阅读时间 10 分钟·2023 年 7 月 8 日

--

想象一下,花费数月心血投入到一个项目中,却看到其内容被搁置。这是许多数据科学家面临的情景。

这种情况不止发生过一次。这让我感到深深的苦涩,疑惑为什么一开始会发生这种事。

这通常发生在 商业目标发生变化时。

项目被降低了优先级,你的贡献也一样。这就是我在这种情况下遇到的情况。对于公司赌注的变化,我无能为力。

其他时候,是因为 利益相关者没有跟进你的工作。

即使你的信息是相关的,当利益相关者无法理解你的洞察不被说服时,也会发生这种情况。可能是因为你在某处搞砸了。好消息是,有方法可以防止这种情况发生

这也发生在我身上,我在这里详细讲述了我如何搞砸了,以及在过程中学到了哪些技能将我的洞察力转化为行动。

图片来源:作者(Midjourney)

成为数据科学家的公式从未改变。它非常直接——归结为:

数学 + 代码 + 商业头脑 + 软技能 = 数据科学家公式

轰!没什么疯狂的。

唯一稍显疯狂的部分是软技能。它们在项目范围的所有阶段都会发挥作用,尤其是当你需要交付你的洞察力并确保它们转化为行动时。公式并不简单明了。

(另外,如果你是初级数据科学家,务必查看我下面的文章,了解作为一名初级数据科学家在 Spotify 学到的 5 个重要课程)

## 我在 Spotify 学到的初级数据科学家五个必备课程(第一部分)

成为数据科学家早期阶段的内行指南,并提升你的技能水平。

towardsdatascience.com

不那么软的技能

我不明白为什么我们称这些技能为软技能。它们比那些技术技能更难培养。没有教科书公式会告诉你确切的方法。

如果这些技能不是由你的仙女教母🪄赐予你的,那么你需要开始努力把它们赋予自己。

如果你的仙女教母看起来不是这样,那么我不想听 — 作者提供的图片(Midjourney)

有些技能你会在被投身实际工作中并亲自动手时学到。而其他技能则会在你与专家们相处时自然获得。

当你想将洞察转化为行动时,软技能尤为重要,因为你需要说服决策者这些洞察为何重要

但我们说的并不仅仅是任何软技能。你需要培养一套特别的技能,以确保你的洞察不会陷入困境。

1. 翻译的艺术

有一天,我需要解释我们进行的复杂 A/B 测试的分析,这涉及使用统计术语如“卡方检验”和“p 值”。

对大多数非技术人员来说,技术术语听起来像是天书。为了让他们更容易理解,我专注于结果是否统计显著。通常,人们知道它与结果的可靠性有关。

当你完成了项目中所有的技术工作后,剩下的就是与利益相关者分享你的劳动成果。

记住你是在对非技术人员讲话

这些人将把你的洞察或模型转化为产品。他们根本不知道什么是随机森林,他们只知道这不是一堆在非洲的树木。

因此,学会 将复杂信息转化为简单语言 以确保你的工作能顺利推进到下一阶段

很容易以为每个人都会理解你认为简单的术语。但仅仅因为他们也在从事或涉及技术工作,并不意味着他们会理解,所以一定要用简单的语言交流。这是传达信息的第一步,否则可能一切都徒劳无功。

作者提供的图片(Midjourney)

2. 说服的力量 🫱🏼‍🫲🏾

我最近花了几个月时间进行深入研究。目标是了解体验的一部分质量,并提出基于数据的改进建议。

一个关键的建议对某些人来说似乎很荒唐,但我的直觉告诉我我们在做对的事。因此,我联系了一个在该项目的完全不同方面工作的团队。我深入研究了他们过去的研究,并收集了任何可以支持我立场的论据。

我最终成功地说服了我的受众,说明我的见解值得追求。如果我一开始没有接受这一点,我将无法通过研究产生影响。

是的,他有三只手,你怎么期望只用两只手完成这些任务?这需要努力,所以你需要尽可能多的手——作者提供的图像(Midjourney)

当你希望你的工作变成真实而有影响的东西时,说服力成为你的新盟友。这不是一种容易学会的技能,但它会帮助你脱颖而出,成为一名卓越的数据科学家。

你的受众在听你说话。这是真的。但这并不意味着他们已经信服。他们为什么要在乎?

他们也必须付出额外的努力来说服为什么值得花时间研究一些东西。因此,你不应该不做同样的事情。每个人的时间都很宝贵。

这里有 4 件事你可以做,也应该做,以引起他们的关注

  1. 不要放弃你强有力的见解。 无论它们对其他人来说多么牵强。相信你的直觉,并为它们辩护,但只有在你对它们的潜力有信心时!!

  2. 提供针对特定受众的有说服力的论点。 通过用他们的语言讲解来打动他们,让你的见解与他们产生共鸣。确保有数据支持。如果你面对的是产品经理,那就展示你的见解如何与产品相关。

  3. 制定战略计划。 你需要概述你提出的将这一愿景付诸实践的关键步骤。

  4. 对成功实施类似项目的团队或公司进行深入研究。强调不落后于人的重要性,并营造紧迫感。

  5. 重复,重复,再重复。 固定的受众需要多次听到某些内容才能整合进来。但也因为你需要将这些见解重复传达给不同的受众。你永远不知道哪个关键利益相关者会接收到这些信息并解锁下一步。

小心 🚨

在你追求影响力的过程中,记住不是所有东西都可以被人为地强行变得有用

如果你发现自己必须过多地说服他人,这可能表明与你的利益相关者的期望和假设不一致。但当你的见解与他们的需求无缝对接,你的故事在他们的背景下闪耀时,就无需说服——影响力会自己证明。

3. 讲故事的魔力 🤯

还记得我之前提到的深度研究吗?

它引起了我部门 VP 的兴趣,因此我和我的经理必须确保故事流畅且完全合理后再进行展示。

我们在这上面花了时间,因为每个细节都必须有数据支持——我们说的任何内容都可能影响重大决策。

在向决策者展示你的见解时,讲故事的技巧是决定它们是否会进入排行榜的终极差异

在几周或几个月内深入一个主题后,区分哪些内容真正有价值分享变得具有挑战性。学习如何挑选重要的数据部分并将其与产品建议联系起来,然后说服决策者采取行动是一项关键技能。

成功的演示是技巧性传递和战略性策划相关且引人注目的内容的结果

到目前为止,你知道你想与股东讨论什么。但你是否知道如何讨论它?

如果你是初级人员,你可能不记得。即使是最资深的技术数据科学家也仍在磨练这一技能。

作者提供的图片(Midjourney)

这些是我在为 VP 的审查准备演示时获得的讲故事技巧

首先,确保你了解业务目标主要战略方向关注领域以及你的项目如何与之对齐。

这将使你创建一个不孤立的故事。它与公司活跃的生态系统相关联。你的研究不是一项独立的工作。它旨在为更广泛的使命和愿景做出贡献。

在演示中清楚地表述你的工作如何直接与这一宏观目标对齐

幻灯片 1 — 执行摘要

它概述了你的工作,并包括关键见解和建议的总结。它为你的观众设定了期望。

这通常应包括:

  1. 关键见解、数字或总结当前问题的观点

  2. 研究目标 + 达成此目标所用的方法

  3. 一张展示推动研究的关键统计数据的图表

  4. 关键见解和建议的要点

执行摘要的模板 – 作者提供的图片

幻灯片 2 — 理由 + 免责声明/警告

  1. 更详细地描述那个关键见解/数字,并解释你的研究试图进一步实现的最终目标

  2. 任何值得分享的内容,股东应了解以理解你的研究,均符合警告的标准。例如,这可能是关于数据质量问题的免责声明

作者提供的图片

幻灯片 3 — 关键见解总结

用数据支持每个关键要素。

🔊 一条建议 🔊: 在每个要点旁边添加一个超链接,指向你深入分析该见解的幻灯片。这使得浏览你的幻灯片简单

作者图片

第 4 页 — 推荐总结

用相应的关键数字支持每个建议。

🔊 一条建议 🔊: 标记相关利益相关者。在这一部分,你需要解释如何将你的建议付诸实践给那个有能力实施的人。

作者图片

正文 — 将你的故事分成章节 + 在每一章末尾插入回顾

每个部分都作为你故事的章节。以下是如何在章节中展示每个发现的示例。

  1. 确保每个部分为下一个部分铺平道路。记住你在讲述一个故事,因此这些需要无缝衔接

  2. 在每章末尾包含回顾。这有助于信息在你观众的脑海中更牢固地留下。

作者图片

最后一页 — 下一步

列出未来需要采取的行动。例如:

“监控 A/B 测试的启动”

作者图片

最后,记住你的故事需要自说自话。 这意味着即使没有你的干预,任何人也应该能够随时获得见解。

讲故事是数据科学家的持续旅程。 随着时间的推移,你将发展出自己的风格。

总结

成为数据科学家的真正成功不仅在于你的技术能力,还在于你有效沟通、影响和激励的能力。你可以通过以下方式做到这一点:

  1. 将技术术语翻译成英语。 简化复杂概念,以确保非技术人员理解你的见解及其价值。

  2. 说服你的观众为什么你的见解值得关注。 你让利益相关者支持你的事业的能力可以将单纯的概念转化为行动,创造持久的影响。

  3. 不断磨练你的讲故事技巧。 成功不仅在于演讲的呈现,还在于内容的精心策划。

掌握这些软技能将推动你的职业生涯迈向新高峰,并使你的工作真正产生影响。

即将推出:揭示终极技能,使数据科学家在技术领域脱颖而出,获得最佳工作

  • 技术领域所有数据科学家共有的一项技能

  • 一项能开启你梦想数据科学工作的大门的技能

  • 一项重要到值得单独讲述的技能

所以,请继续关注,因为你不会想错过。

好吧,现在我开始听起来像个吸引点击的标题了。

下次见👋🏼

我有礼物送给你🎁!

注册我的 newsletter K’s DataLadder,你将自动获得我的终极 SQL 备忘单,其中包含我在大型科技公司每天使用的所有查询+另一个神秘礼物!

我每周分享作为科技行业数据科学家的经历,以及实用的技巧、技能和故事,旨在帮助你提升自己——因为没人真正了解,直到他们自己亲身经历!

如果你还没有做的话

  • 订阅我的YouTube频道。新视频很快就会发布!

  • InstagramLinkedInX上关注我,选择你喜欢的方式

很快见!

逐步指南:通过从单变量分布中采样生成合成数据

原文:towardsdatascience.com/step-by-step-guide-to-generate-synthetic-data-by-sampling-from-univariate-distributions-6b0be4221cb1

学习如何创建合成数据,以防你的项目数据不足或用于模拟

Erdogan TaskesenTowards Data Science Erdogan Taskesen

·发表于 Towards Data Science ·阅读时间 10 分钟·2023 年 3 月 27 日

--

图片来源:Debby HudsonUnsplash

数据是数据科学项目中的燃料。但如果观察数据稀少、昂贵或难以测量怎么办?合成数据可以是解决方案。合成数据是人工生成的数据,模拟真实世界事件的统计特性。我将演示如何通过从单变量分布中采样来创建连续合成数据。 首先,我将展示如何通过模拟评估系统和过程,在此过程中我们需要选择概率分布并指定参数。其次,我将演示如何生成模拟现有数据集特性的样本,即根据概率模型分布的随机变量。所有示例均使用 scipy distfit 库创建。

如果你觉得这篇文章有帮助,请使用我的 推荐链接 继续无限制地学习并注册 Medium 会员。此外, 关注我 以便及时获取我的最新内容!

合成数据 — 背景。

在过去十年中,数据量迅速增长,并导致了一个认识,即数据的质量比数量更为重要。更高的质量可以帮助得出更准确的结论和做出更明智的决策。许多组织和领域中,合成数据都能发挥作用,但有一个特别重视合成数据的领域,那就是自动驾驶汽车。在这里,数据被生成用于许多边缘案例,随后用来训练模型。像 Gartner 这样的公司强调了合成数据的重要性,Gartner 预测真实数据将很快被超越[1]。周围已经有很多清晰的例子,如由生成对抗网络(GANs)生成的虚假图像。在这个博客中,我将不会关注由 GANs 生成的图像,而是关注更基本的技术,即基于概率分布创建合成数据。

合成数据可以通过两大类抽样技术来创建:

  1. 概率抽样; 创建与真实数据分布紧密相似的合成数据,这对于训练机器学习模型和进行统计分析非常有用。

  2. 非概率抽样; 包括在没有已知选择概率的情况下选择样本,例如便利抽样、滚雪球抽样和配额抽样。这是一种快速、简单且廉价的数据获取方式。

我将重点讨论 概率抽样,其中估计总体的分布参数是关键。换句话说,我们在单变量数据集的情况下寻找最佳拟合的理论分布。通过估计的理论分布,我们可以生成新的样本,即我们的合成数据集。

概率密度函数概述。

找到最适合模拟真实世界事件的理论分布可能具有挑战性,因为存在许多不同的概率分布。像distfit [2]这样的库在这种情况下非常有帮助。

## 如何找到最适合你数据的理论分布

了解基础数据分布是数据建模的重要一步,并有许多应用,例如…

towardsdatascience.com

图 1 展示了概率密度函数(PDF)的一个很好的概述,其中捕捉了分布的“规范”形状。这样的概述有助于更好地理解和决定哪种分布可能最适合特定的使用案例。在接下来的两个部分中,我们将尝试不同的分布及其参数,看看我们能多好地生成合成数据。

合成数据是使用统计模型生成的人工数据。

图 1. 概率密度函数及其参数概述。创建者:Rasmus Bááth (2012)

创建用于模拟的合成数据。

使用合成数据是生成大型和多样化数据集以进行模拟的理想选择,这使得测试和探索不同场景成为可能。这有助于获得通过其他手段可能难以或不可能获得的见解和知识,或者在我们需要确定系统和过程的边缘情况时。然而,创建合成数据可能具有挑战性,因为它需要通过使用理论分布和人口参数来模拟现实世界事件

使用合成数据,我们的目标是通过估计理论分布和人口参数来模拟现实世界事件

为了演示合成数据的创建,我创建了一个假设用例,我们在安全领域工作,需要了解网络活动的行为。一位安全专家提供了以下信息;大多数网络活动在 8 点开始,并在 10 点左右达到高峰。一些活动会在 8 点之前出现,但不多。下午,活动逐渐减少,并在下午 6 点左右停止。然而,1 点到 2 点之间也有一个小高峰。 请注意,一般来说,描述异常事件比描述正常/预期行为要困难得多,因为正常行为是最常见的,因此观测数据的比例最大。让我们将这些信息转化为统计模型。

将领域知识转化为统计模型。

根据描述,我们需要决定最佳匹配的理论分布。然而,选择最佳理论分布需要调查许多分布的特性(见图 1)。此外,你可能需要多个分布;即概率密度函数的混合。在我们的示例中,我们将创建两个分布的混合,一个用于早晨,一个用于下午活动。

描述早晨:“大多数网络活动在 8 点开始,并在 10 点左右达到高峰。一些活动会在 8 点之前出现,但不多。

为了对早晨网络活动进行建模,我们可以使用正态分布。它是对称的,没有重尾。我们可以设置以下参数:均值为上午 10 点,且相对较窄的范围,如 sigma=0.5。图 2 中显示了一些具有不同均值和 sigma 参数的正态 PDF。尝试感受 sigma 参数变化对斜率的影响。

图 2. 具有各种参数的正态分布。来源:维基百科

下午描述:“活动逐渐减少并在下午 6 点左右停止。然而,1 到 2 点之间也有一个小峰值。”

适合下午活动的分布可能是一个具有重右尾的偏斜分布,可以捕捉逐渐减少的活动。韦布尔分布可以是一个候选,因为它用于建模具有单调递增或递减趋势的数据。然而,如果我们并不总是期望网络活动单调递减(因为在周二或其他情况中不同),那么考虑如伽马这样的分布可能更好(图 3)。在这里,我们也需要调整参数,以便它最符合描述。为了更好地控制分布的形状,我更喜欢使用广义伽马分布

图 3. 一个具有不同参数的伽马分布。来源:维基百科

在下一部分中,我们将尝试这两种候选分布(正态分布和广义伽马分布),并设置参数以创建代表网络活动用例的 PDF 混合。

优化参数以创建最符合场景的合成数据。

在下面的代码部分,我们将从均值为 10(代表上午 10 点的峰值)和标准差为 0.5 的正态分布中生成 10,000 个样本。接下来,我们从广义伽马分布中生成 2,000 个样本,我将第二个峰值设置在loc=13。我们本来也可以选择loc=14,但这会导致两个分布之间的间隔更大。下一步是将两个数据集结合起来并打乱它们。请注意,打乱并不是必须的,但如果不这样做,样本将首先按 10,000 个正态分布样本排序,然后按 1,000 个广义伽马分布样本排序。这种顺序可能会在对数据集进行任何分析或建模时引入偏差。

import numpy as np
from scipy.stats import norm, gengamma
# Set seed for reproducibility
np.random.seed(1)

# Generate data from a normal distribution
normal_samples = norm.rvs(10, 1, 10000)
# Create a generalized gamma distribution with the specified parameters
dist = gengamma(a=1.4, c=1, scale=0.8, loc=13)
# Generate random samples from the distribution
gamma_samples = dist.rvs(size=2000)

# Combine the two datasets by concatenation
dataset = np.concatenate((normal_samples, gamma_samples))
# Shuffle the dataset
np.random.shuffle(dataset)

# Plot
bar_properties={'color': '#607B8B', 'linewidth': 1, 'edgecolor': '#5A5A5A'}
plt.figure(figsize=(20, 15)); plt.hist(dataset, bins=100, **bar_properties)
plt.grid(True)
plt.xlabel('Time', fontsize=22)
plt.ylabel('Frequency', fontsize=22)Let’s plot the distribution and see what it looks like (Figure 3). Usually, it takes a few iterations to tweak parameters and fine-tuning. 

图 4. 正态分布和广义伽马分布的概率密度函数混合。作者提供的图像。

我们使用两种分布的混合来创建合成数据,以模拟特定人群网络活动的正常/预期行为(图 4)。我们在上午 10 点模拟了一个主要的峰值,网络活动从早上 6 点开始,一直到下午 1 点。第二个峰值在下午 1 到 2 点左右,右尾很重,延续到晚上 8 点。下一步可能是设置置信区间并追求异常值检测。有关异常值检测的更多细节可以在以下博客 [3] 中找到:

## 使用分布拟合进行异常值检测

学习如何使用概率密度函数检测异常值,以实现快速、轻量的模型和可解释的结果。

[towardsdatascience.com

创建与真实数据分布高度一致的合成数据。

到目前为止,我们创建了允许通过模拟探索不同场景的合成数据。这里,我们将创建与真实数据分布高度一致的合成数据。作为演示,我将使用 Seaborn消费小费数据集 [4] 并使用 distfit library [2] 估算参数。如果你是第一次估算概率密度函数我推荐阅读关于 distfit 的博客。 tips 数据集仅包含 244 个数据点。让我们首先初始化库,加载数据集并绘制值(见代码部分)。

# Install distfit
pip install distfit

# Initialize distfit
dfit = distfit(distr='popular')

# Import dataset
df = dfit.import_example(data='tips')

print(df)
#        tip
# 0      1.01
# 1      1.66
# 2      3.50
# 3      3.31
# 4      3.61

# 239    5.92
# 240    2.00
# 241    2.00
# 242    1.75
# 243    3.00
# Name: tip, Length: 244, dtype: float64

# Make plot
dfit.lineplot(df['tip'], xlabel='Number', ylabel='Tip value')

对数据集进行可视化检查。

加载数据后,我们可以进行可视化检查,以了解范围和可能的异常值(图 5)。244 个小费的范围主要在 2 到 4 美元之间。基于这个图,我们还可以建立预期分布的直观印象,当我们将所有数据点投影到 y 轴时(我将稍后演示)。

图 5. 244 位客户的消费小费数据集。

distfit 的搜索空间设置为流行的 PDF,平滑参数设置为 3。样本量过少会使直方图出现不平滑,并导致分布拟合不佳。

# Import library
from distfit import distfit

# Initialize with smoothing and upperbound confidence interval
dfit = distfit(smooth=3, bound='up')

# Fit model
dfit.fit_transform(df['tip'], n_boots=100)

# Plot PDF/CDF
fig, ax = plt.subplots(1,2, figsize=(25, 10))
dfit.plot(chart='PDF', n_top=10, ax=ax[0])
dfit.plot(chart='CDF', n_top=10, ax=ax[1])

# Show plot
plt.show()

# Create line plot
dfit.lineplot(df['tip'], xlabel='Number', ylabel='Tip value', projection=True)

最佳拟合的 PDF 是beta(图 6,红线)。上界置信区间 alpha=0.05 为 5.53,根据视觉检查,这似乎是一个合理的阈值(红色垂直线)。

图 6. 左:PDF,右:CDF。顶部 5 个拟合的理论分布以不同颜色显示。最佳拟合为 Beta,用红色标记。(图片由作者提供)

找到最佳分布后,我们可以将估算的 PDF 投影到我们的折线图上,以获得更好的直观感受(图 7)。请注意,PDF 和经验 PDF 与图 6 中完全一致。

图 7. 244 位客户的消费小费数据集。基于当前数据估算的经验 PDF。理论 PDF 是最佳拟合分布。(图片由作者提供)

使用最佳拟合分布的估计参数,我们可以开始创建关于小费的合成数据(见下方代码部分)。让我们创建 100 个新样本并绘制数据点(见图 8)。合成数据提供了许多机会,即它可以用于训练模型,也可以帮助我们了解诸如使用小费储蓄特定金额所需的时间等问题。

# Create synthetic data
X = dfit.generate(100)

# Ploy the data
dfit.lineplot(X, xlabel='Number', ylabel='Tip value', grid=True)

图 8。合成数据。我们可以看到值在 2-4 之间,有一些离群值。红色水平线是先前估计的 alpha=0.05 的置信区间。经验 PDF 是基于当前数据估计的。理论 PDF 是基于我们之前的拟合。这允许快速比较生成的数据和拟合的理论 PDF。(图片由作者提供)

最后的话。

我展示了如何通过使用概率密度函数以单变量方式创建合成数据。使用distfit库,可以评估 89 种理论分布,估计的参数可以用于模拟现实世界事件。尽管这很棒,但创建合成数据也存在一些局限性。首先,合成数据可能无法完全捕捉现实世界事件的复杂性,缺乏多样性可能导致模型在训练时无法泛化。此外,由于不正确的假设或参数估计,合成数据可能会引入偏差。务必始终对你的合成数据进行合理性检查。

保持安全。保持冷静。

干杯,E.

如果你觉得这篇文章有帮助,可以使用我的 推荐链接 继续无限学习并注册 Medium 会员。还有, 关注我 以保持最新内容的更新!

软件

让我们联系!

参考文献

  1. Gartner, Maverick Research: 忘掉你的真实数据——合成数据是 AI 的未来, Leinar Ramos, Jitendra Subramanyam, 2021 年 6 月 24 日。

  2. E. Taskesen, 如何找到最适合你数据的理论分布, 2023 年 2 月 Medium。

  3. E. Taskesen, 在单变量数据集中使用分布拟合进行离群值检测, Medium 2023

  4. Michael Waskom, Seaborn, Tips 数据集, BSD-3 许可证

理解的垫脚石:知识图谱作为可解释链式思维推理的支撑

原文:towardsdatascience.com/stepping-stones-to-understanding-knowledge-graphs-as-scaffolds-for-interpretable-chain-of-thought-2b9139c28c60

Anthony Alcaraz数据科学前沿 Anthony Alcaraz

·发表于数据科学前沿 ·7 分钟阅读·2023 年 11 月 21 日

--

使用人工智能软件来增强本文的语法、流畅性和可读性。

大型语言模型(LLMs)在大量文本数据的训练下,引发了 AI 领域的革命。它们仅凭简短文本提示即可生成极具 eloquent 和连贯的语言,开辟了从创意写作到对话助手等各个领域的新视野。

然而,单凭语言表达的掌握并不等同于真正的智能。大型语言模型(LLMs)仍然缺乏对概念的语义理解和情境理解及复杂问题解决所需的逻辑推理能力。它们的知识仍局限于从训练语料中 discerned superficial patterns,而不是关于真实世界的基础事实。

当我们对这些模型提出更多开放性、多方面的问题时,它们的局限性变得越来越明显。它们无法从不同文档中逻辑性地综合细节,或进行多步骤推理以得出答案。

一旦查询开始偏离训练数据的分布,就会出现虚幻或矛盾的回应。

为了应对这些问题,AI 社区将重点转向了检索增强生成(RAG)框架。这些系统旨在将语言模型的语言能力与对外部知识来源的快速、针对性访问相结合,以将其基础于事实背景中,而不是虚幻的内容。

目前大多数现有架构使用语义相似性从文本语料库的向量表示中检索补充信息。然而,这在检索的段落与实际查询上下文之间的模糊相关性上存在困难。当将段落浓缩为单一不透明的向量时,关键信息容易丢失,缺乏上下文链接。通过逻辑推理将离散事实结合成连贯叙事仍然很艰难。

这强调了纳入封装现实世界实体及其之间关系的结构化知识源的必要性。知识图谱满足了这一需求——将事实编码为可以沿解释路径遍历的互联节点和边。然而,有效地将语言模型的自由形式推理建立在结构化图谱上存在接口挑战。创造性地将神经方法与符号表示桥接仍然是一个未解问题。

一种在这一方向上展现出希望的新兴技术是思维链(CoT)提示。CoT 促使语言模型以逐步推理链条的形式展示其推理过程。每一个连接变得清晰可见,增强了透明度。然而,在完全自由形式的语言空间中,长时间的历史往往会迅速破裂。知识图谱可能提供缺失的支撑结构,以给这些逐渐展开的推理轨迹提供结构。

明确地沿知识图谱路径追踪 CoT 步骤可能会实现基于事实链条的逻辑推理。然而,找到非结构化神经输出与结构化符号知识之间的正确对齐仍然是一个未解的挑战。在这方面的创新为融合两种方法的优点提供了希望——将符号表示与基于真实世界实体的可靠推理链条结合,通过向量空间流畅地连接,从而实现高效的统计推断。

文章的其余部分将探讨知识图谱与 CoT 推理在大型语言模型中的有前景的交集,以实现更稳健的情境智能。我们将深入研究利用每种方法的互补优势,同时减轻它们在孤立状态下的缺陷的技术。

I. 知识图谱用于稳健的少样本学习

[## 新研究证明知识图谱显著提高大型语言模型的准确性]

自然语言接口到数据库一直是工业界和学术界的圣杯。最近,进展……

ai.plainenglish.io ## 向量搜索并非你所需的一切

介绍

[towardsdatascience.com

现有的大多数 RAG 系统仅依赖于段落嵌入进行语义相似性匹配。然而,这些系统在处理模糊相关性和无法联合分析分散在多个段落中的连接事实方面存在困难。知识图谱通过保留符号事实和关系来解决这些问题,从而支持可解释的多跳推理。

多样的图算法用于多用途推理

知识图谱为我们提供了一整套新的算法,优化用于不同的推理模式:

  • 像个性化 PageRank 这样的图遍历算法通过分析实体之间的间接连接来支持灵活的关联推理。这支持从跨多个边的推理链中推导出新的关系。

  • 为搜索优化的算法(例如,近似最近邻)可以高效查询与特定实体相关的事实。这有助于精确的事实检索。

  • 图总结算法可以简明地提炼出包含最相关信息的子图,以简化推理。这减少了噪音并提高了焦点。

优化的知识图谱嵌入

此外,知识图谱中的元素如实体、关系和文本也可以编码到向量空间中,从而支持数学操作:

  • 传递性嵌入通过在关系组成中保持等价性来改善跨多跳推理链的演绎推理。

  • 层次嵌入编码了实体之间的分类层级,允许基于继承的推理。这从祖先类继承事实。

  • 大型变换器语言模型中的上下文嵌入捕捉了实体和关系文本属性中的语义细微差别。

丰富的符号表示与灵活的向量空间之间的合作为少量学习提供了最佳基础——结构化的知识用于支撑显式逻辑推理,通过动态流畅的向量空间提炼出显著的模式。

II. 用结构化知识图谱增强思维链

## 在混乱的背景下通过思维链提示实现 LLMs 的结构化推理和…

大型语言模型(LLMs)展示了令人印象深刻的少量学习能力,能够快速适应新任务……

[towardsdatascience.com [## 思维图:使用大型语言模型解决复杂问题

我们介绍了 Graph of Thoughts(GoT):一个提升大型语言模型(LLMs)提示能力的框架…

## 图上的推理:可信且可解释的大型语言模型推理

大型语言模型(LLMs)在复杂任务中展示了令人印象深刻的推理能力。然而,它们缺乏…

arxiv.org

链式推理(CoT)提示指导语言模型通过解释性推理步骤链展示其推理过程。然而,当推理跨越更长的历史时,一致性通常会在自由形式的语言空间中解构。知识图谱可以提供缺失的结构。

显式编码概念和关系

知识图谱将概念编码为相互链接的符号节点,捕捉它们之间的关系。通过这个明确的网络遍历解释路径可以支撑链式推理。像 Graph-of-Thoughts(GoT)这样的近期方法探索组装情境图来建模不断演变的链式推理步骤。

超越建模,结构化表示可以参与引导推理:

  1. 查询本体:初始本体查询建立定义和高层次背景,可以框定推理。

  2. 遍历关系:图算法可以收集与每个 CoT 步骤相关的连接事实。

  3. 更新表示:嵌入编码提取的细节,集中注意力。

协调混合推理

将神经 CoT 提示与结构化知识图谱整合需要协调分布式推理模块:

  1. 管理者:排序模块执行,平衡语言模型请求与检索。

  2. 提示者:通过 CoT 提示从语言模型中引出自由形式的理由。

  3. 检索器:使用个性化 PageRank 等算法收集相关的图形细节。

  4. 解析器:将检索到的事实转换为自然语言或向量。

  5. 评分器:评估检索到的事实在当前上下文中的相关性。

  6. 融合器:将显著知识与更新提示结合,为下一轮准备。

模块化架构允许结合神经和符号方法的优点。语言模型思维流畅,而图形保留逻辑——彼此弥补对方的局限性。

编排混合推理

协调结构化知识与流体向量推断之间的分阶段互动是解锁结合以下内容的新推理能力的关键:

  • 使用嵌入的可扩展模式识别来解释显式符号建模的可解释性。

  • 公理知识的逻辑合理性与神经方法的自适应即兴发挥。

  • 图谱遍历提供的可解释性与向量的高效计算。

这种综合方法的创新承诺比单独使用任何一种方法提供更可靠、多功能和透明的推理。这些合作伙伴关系为情境智能开辟了新前沿。

III. 当前差距与未来方向

## 知识图谱转换器:为演变中的知识构建动态推理

知识图谱通过表示事实为相互连接的实体,已成为增强的关键技术...

## 解锁图谱在 AI 推理中的力量

图谱在现代数据驱动的商业世界中无处不在。

解锁图谱在 AI 推理中的力量

尽管知识图谱有助于支撑推理,但要实现其全部潜力面临如次优构建、对齐、个性化及处理演变等障碍:

综合的高保真知识图谱

手动策划涵盖多样化领域的广泛高质量知识图谱会遇到扩展瓶颈。同时,开源图谱存在稀疏、不一致和噪音问题,不适合支持健全的推理链。清晰的本体知识与嘈杂的网络提取结合带来了整合难题。

平滑集成向量和符号空间

将知识图谱的符号结构与语言模型的潜在向量空间桥接,以实现信息的无缝交换并非易事。像直接向量查找这样的简单方法难以充分捕捉符号语义。更先进的技术如图神经网络展现了优雅地将图谱嵌入语言模型向量空间的潜力,从而实现紧密耦合的推理。但研究仍处于起步阶段。

个性化和当前时态图谱

静态知识图谱无法有效反映个体用户的独特背景,限制了与个人世界知识对齐的个性化推理。构建可定制的用户特定知识图谱依然成本过高。同时,静态图谱也会逐渐过时,无法跟踪不断变化的现实世界状态和事件,这些对当代推理至关重要。准确反映我们短暂环境的动态图谱至关重要。

探索克服限制的创新

然而,创新技术通过融合技术结合策划和提取的知识、改进的基础算法深度结合符号和神经推理、智能助手辅助的可定制动态图谱构建以及持续更新表示的流学习,展现了缓解这些限制的潜力——这些都共同实现了综合推理的共生潜力。

资料来源:

## 激发语言智能:从链式思维推理到语言的指南…

大型语言模型(LLMs)显著提升了语言智能领域,这一点通过…

arxiv.org

作者提供的图片

3D 跟踪的立体视觉系统

原文:towardsdatascience.com/stereo-vision-system-for-3d-tracking-ce8eaca3a40a?source=collection_archive---------1-----------------------#2023-01-08

你只需要两只眼睛

Sébastien GilbertTowards Data Science Sébastien Gilbert

·

关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 1 月 8 日

--

照片由 Adriano Pinto 提供,来源于 Unsplash

就像地球上绝大多数有视力的动物一样,我们有两只眼睛。我们进化中的这一奇妙特性使我们能够以三维的方式看待环境。

要从场景中获取 3D 信息,我们可以通过至少两台相机协同工作来模拟双眼视觉。这种设置被称为立体视觉系统。当相机经过正确校准后,每台相机都会对特定特征点的 3D 坐标提供约束。通过至少两台经过校准的相机,可以计算特征点的 3D 坐标。

在本文中,我们将校准一对相机,并使用此校准来计算在一系列图像中跟踪的特征点的 3D 坐标。你可以在这个仓库找到相应的代码。

立体视觉系统

图 1 显示了我使用的立体视觉系统。如你所见,没有什么花哨的:一对通过 3D 打印部件、绑带和热熔胶固定在一起的网络摄像头。

图 1:立体视觉系统。作者提供的图像。

正好我有两台相同型号的网络摄像头闲置在抽屉里,但并不一定需要完全相同的相机。由于相机是独立校准的,它们可以具有不同的内在参数,并且仍然在立体视觉系统中发挥作用。

相机的校准

投影矩阵

相机的校准归结为计算它们的投影矩阵。针孔相机模型的投影矩阵是一个 3x4 矩阵,它允许从世界参考框架中的 3D 坐标计算特征点的像素坐标。

在公式(1)中,i 指代相机索引。在一个由两台相机组成的系统中,i 属于{1, 2}。

Pᵢ 是相机 i 的 3x4 投影矩阵。

(uᵢ, vᵢ) 是特征点的像素坐标,从相机 i 看到的情况。 (X, Y, Z) 是特征点的 3D 坐标。

标量 λᵢ 是保持方程同质性的缩放因子(即,两边向量的最后一个元素是 1)。它的存在来自于将 3D 点投影到 2D 平面(相机传感器)时信息的丧失,因为多个 3D 点映射到同一个 2D 点。

假设我们知道相机的投影矩阵,并且我们有一个特征点的像素坐标,我们的目标是隔离(X, Y, Z)。

将(3)代入(2)的前两行:

公式(6) 表明,每个相机视图为我们提供两个未知数(X, Y, Z)的线性方程。如果我们至少有两个相机视图的相同 3D 点,我们可以通过求解一个过定的线性方程组来计算 3D 坐标。

太棒了!但我们如何计算投影矩阵呢?

投影矩阵的计算

要计算相机的投影矩阵,我们需要大量已知的 3D 点及其对应的像素坐标。我们使用一个棋盘格标定图案,尽可能精确地放置在测量距离上的立体设备前。

图 2:在已知位置的棋盘格标定图案。作者提供的图像。

该仓库包括标定模式图像,适用于两台相机。

图 3:60 cm 距离摄像头的棋盘格标定模式图像。图片由作者提供。

你可以使用这个 Python 程序运行整个标定过程。

每个标定图像中的方形交点通过 CheckerboardIntersections 类的一个实例进行检测,该类在我之前的文章中介绍过。

在这种情况下,我们将交点检测器的参数设置得相对灵敏,以便检测所有真实交点,并且有一定量的假阳性(换句话说,完美的召回,合理的精度)。由于标定是一次性的过程,我们可以承受手动去除假阳性的工作。程序会遍历标定模式图像并要求用户选择假阳性。

图 4:找到的交点,手动去除假阳性前后的情况。图片由作者提供。

正如我们在我关于相机径向失真的补偿文章中看到的,原始交点坐标必须是未失真的。径向失真模型之前已经为两台相机计算,相应的文件已在仓库中。未失真的坐标是我们用来构建针孔相机模型的投影矩阵的坐标。

此时,我们有 7(不同距离的拍摄)x 6 x 6(方形交点)= 252 对应于每台相机的 3D 点和像素坐标。

为了计算投影矩阵的条目,我们将重新开始使用方程(1),但这次假设我们知道(u, v)ₖ 和(X, Y, Z)ₖ,并且我们要解决 P 的条目。下标 k 指的是(pixel_coords, XYZ_world)对的索引。标量 λₖ(每个点一个)也是未知的。我们可以通过一些操作将 λₖ 从线性方程组中消除:

方程(8)和(9)可以写成 Ap = 0 的形式:

方程 (10) 显示每个对应关系提供了 12 个未知数中的两个齐次线性方程。至少需要 6 个对应关系来解出 P 的条目。我们还需要我们的 3D 点是非共面的。通过 7 个平面中的 252 个对应关系,我们是安全的。

在执行校准程序后,我们可以验证投影矩阵是否正确地将已知的 3D 点投影回其未畸变的像素坐标。

图 5:图像中棋盘格 3D 点的投影。图像由作者提供。

在图 5 中,蓝色点是经过径向畸变补偿后由交点检测器找到的点。黄色圆圈是图像中 3D 点的投影。我们可以看到,两台摄像头的投影矩阵表现良好。请注意,偏离标注点与棋盘格交点不重合,这是由于径向畸变补偿造成的。

3D 跟踪

我们现在可以使用校准后的立体系统来计算特征点的 3D 位置。为了演示这一点,我们将跟踪一个易于检测的特征点(一个红色方块的中心)在一系列图像中的位置。

你可以在这里找到跟踪程序。

图 6:左:跟踪的红色方块的图像。右:检测到的斑点。图像由作者提供。

图 6 展示了跟踪红色方块的检测示例。简而言之,方块是通过首先识别图像中蓝色成分占主导的区域来跟踪的,因为红色方块周围的区域是蓝色的。然后,在蓝色主导区域内找到红色成分占主导的区域。详细信息请参阅代码

图 7:红色方块的中心在 3D 中被跟踪。坐标单位为厘米。动画由作者提供。

使用两台摄像头的未畸变像素坐标及其对应的投影矩阵,可以计算出每张图像中特征点的 3D 位置,如上面的动画所示。

结论

我们用一对网络摄像头构建了一个简单的立体视觉系统。我们通过补偿径向畸变并计算投影矩阵来校准这两台摄像头。我们可以使用经过校准的系统来跟踪一系列图像中一个特征点的 3D 位置。

请随意尝试代码

如果你有立体视觉的应用想法,请告诉我,我会非常感兴趣了解!

网络交流让我找到工作,即使我曾被拒绝,看看我是怎么做到的。

原文:towardsdatascience.com/stop-being-bad-at-networking-6-must-do-steps-to-stand-out-in-a-crowded-data-science-job-market-cec48aa847be

别低估 LinkedIn 网络交流的重要性,它阻碍了你找到数据科学工作的机会。

Khouloud El AlamiTowards Data Science Khouloud El Alami

·发表于 Towards Data Science ·12 分钟阅读·2023 年 11 月 5 日

--

欢迎来到 LinkedIn 101 — 图片由作者提供 (Dall.E)

三年前,当我还是一名学生时,我申请了一个在科技领域非常有竞争力且真的很酷的数据科学实习职位,这是我非常渴望的。

剧透警告:我被拒绝了。两次

但我还是得到了实习机会。

怎么做?我把 LinkedIn 网络交流技巧付诸实践。在这篇文章中,我将向你展示如何自己做到这一点,并让你更接近梦想中的工作。然后,我会详细讲述我获得这个实习职位的故事!

现在,作为一名 Spotify 数据科学家,我每天都被渴望成为数据科学家的消息淹没。他们知道,就像我一样,LinkedIn 上的网络交流不仅有用,而且对职业发展至关重要。

没有它你不会走得远。

如果你是一名寻找数据科学职位的求职者,这篇文章可能会改变你的游戏规则。在这里,我将深入探讨有效的网络交流策略,这些策略帮助我获得了想要的机会,即使在申请过程中被拒绝之后。

说到 LinkedIn,你可以在这里找到我。

职场竞争激烈,极其竞争,而且不总是公平的。

根据《福布斯》,75% 的简历 被申请跟踪软件 招聘经理看到之前就已被拒绝。

这对你意味着两件事:

  1. 如果你不利用社交关系,你的职业生涯可能不会走得太远。

  2. 如果你不这样做,确保别人会做到,他们会让你一无所有。

你必须做的远远不止按下发送按钮。

我也经历过——发了数千份申请,但都无济于事。

要成功,你需要脱颖而出,为此你需要一个策略。

仍然不信服?这里有四个统计数据会让你完全重新思考你如何申请工作以及为什么人际网络如此重要:

注意 — 在求职的情况下,人际网络只能帮助你让申请更引人注目,其余的还是要靠你自己。如果你对你所瞄准的工作不够资格,人际网络就没有意义了。

就我而言,我被录用是因为我已经具备了技能,唯一的问题是让他们知道我存在。这就是人际网络改变游戏规则的地方。

申请陷入困境 vs. 人际网络的力量 — 作者(Dall.E)的图片

你在人际网络方面很差

或者至少,你很可能不擅长这方面。大多数人不知道如何进行人际网络。我甚至会说,很多人在人际网络方面表现得很糟糕。你可能也是其中之一。

这没关系。我们从学校毕业时没人给我们一本操作手册。我自己也不是很棒,但当我意识到成功需要这些技巧时,我很早就学到了许多窍门。这确实有效。

当你还在求职者的另一边游泳时,人际网络更加困难——这是一个几乎没有职业经验的求职者的侧面。

让我们做一个思维实验——假设你想申请xyz公司,你已经找到了一些可以在 LinkedIn 上联系以获取更多信息或帮助的人。

如果你发送的消息内容是:“我们可以聊聊你的数据科学职业历程吗?”或者“我很想了解你是如何做到现在的位置的?”,那你做错了。

你可能已经在自我设限,因为你很可能以错误的方式接近他们。

我怎么知道的?我曾经在池子的两边。当我还是一个寻找第一个工作的挣扎的学生时,现在作为一个被希望获得第一个工作的人的 Data Scientist。

这是我用来正确开展人际网络的策略。

步骤 1. 利用您的网络

您的网络是一个金矿。这里的“网络”指的是您已经认识的人:朋友、家人、同学和同事。

让他们了解您的职业目标和抱负。我相信您一定在动态中见过一两条类似的帖子,里面的人在向他们的网络寻求更多曝光。

不要羞于做同样的事情。在最坏的情况下,您将继续失业,这与目前的状态没有区别。在最好情况下,您将获得一个机会,那就是您所追求的胜利!

您可以做的具体事情:

  • 列出您认为可以帮助或指导您的亲密联系人。也许是以前的同事或同学。

  • 组织一次随意的咖啡会面或虚拟见面会,讨论您的职业目标。

  • 明确地问“您是否认识[行业/角色]中的任何人,我应该与他们交谈?”

  • 发布一条询问您网络曝光的帖子。 您可以在其中分享您的抱负和简历。

您会惊讶于您的网络中有多少人会认识某些人或可以将您推荐给一个符合您寻找的角色的机会。

网络中的某人的推荐至少可以提升您被注意到的机会。

步骤 2. 投网更广

当这不起作用时,就该投网更广了。

致力于扩展您的网络。

如何做到?

  • 注册数据/人工智能或感兴趣公司的网络研讨会、工作坊或研讨会。 我在加入俱乐部之前参加了很多 Spotify 的网络研讨会,不要害怕这样做。参加后,您可以通过 LinkedIn 与演讲者或参与者建立联系,发送一条提到活动的个性化消息。

  • 参加数据/人工智能活动或会议。 这将让您获得幕后访问权限,并有机会结识相关人员。确保与尽可能多的人建立联系。您永远不知道谁会给您带来胜利的机会!

在与新朋友建立联系时,总是要带上背景介绍自己。与其简单地说“你好”,不如说“你好,我参加了同一个关于 AI 进展的网络研讨会,我很想与您讨论 x 事”。

让我们更深入地探讨如何在 LinkedIn 上与您的网络建立联系⬇

步骤 3. 不要含糊其辞。问实质性的问题

现在您准备扩展您的网络。您找到了一些有兴趣联系的人。那么该如何做呢?

最宝贵的经验教训——不要在提问时含糊其辞!

具体明确地说明,您成功的机会会增加 —— 图片来源:作者(Dall.E)

您要尽量减少您与专业人士之间的摩擦。

泛泛的问题会得到泛泛的回答, 或者更糟的是,根本没有回答。如果您要联系某人,请做好功课。

我必须承认,我总是避免回答那些问我“在 Spotify 工作是什么感觉?”、“能告诉我你的职业经历吗?”、“你做了什么才能成为今天的你?”这样的问题。

这些问题听起来像是存在主义的问题,需要我花费大量的精力来回答,更重要的是 — 时间,这对我来说是最宝贵的资产。

所以规则 #1,直截了当。

与其问“在 Spotify 工作是什么感觉?”,不如尝试问“我注意到 Spotify 最近整合了 AI 以改善播放列表建议。你能分享一下你在这个项目中的角色吗?”,但前提是你确定对方确实参与了这个项目。

我在 Spotify 负责汽车体验方面的工作,因此对播放列表的情况了解不多。将你的问题专门针对你联系的对象。使问题对他们相关,以最大限度地提高得到回复的机会。

你可以做的一些额外事情:

  • 在联系之前,花 10–15 分钟研究一下对方的当前角色、公司或任何最近的成就。 这是你想要你的社交媒体跟踪技能最闪光的时候( — 而不是你的前任)。

  • 提出展示你已做功课的问题: “我看到你最近的项目涉及[具体细节]。你是如何处理[具体方面]的?”

  • 避免宽泛的问题。与其问“对像我这样的人有什么建议?”,不如问“我应该专注于哪些具体技能才能在[具体角色/行业]中表现出色?”

这应该能让你顺利开始!

第 4 步。专业礼仪是必须的

在做这些事情时,你要注意信息的呈现方式。

在给你不认识的人发消息时,要记住两个重要规则:

  1. 始终仔细检查名字。 这看起来可能微不足道,但它对信息的接收方式以及信息是否会被接收有很大的影响。我遇到过许多人弄错我的名字,这让我感到非常不快。既然你没有花时间正确拼写他们的名字,为什么他们要花时间帮助你呢?这是他们的身份,所以从一开始就要做对。

  2. 当他们给你提供了你所要求的信息时,请回复。 回复是一种基本的礼仪。如果有人从他们的日程中抽出时间为你提供见解或建议,务必表示感谢。我遇到过许多人在得到他们想要的东西后完全不再回复我的消息,这也让人感到非常不悦。

为什么这些都很重要?

不要让他们的信息陷入沉寂,表示感激 — 图片由作者提供(Dall.E)

这是合乎体面的做法。这表现出你对联系人的尊重和关怀。如果他们第一次回复了,你更有可能在未来得到他们的帮助。

不要破坏未来的机会。

第 5 步。保持更新

如果有人帮助了你,确保他们投入了一些时间和精力,所以要确保通过提供进展更新来跟进。

人们会欣赏与你一起庆祝你的胜利,尤其是当他们在其中发挥了作用时。

他们会看到努力帮助你所带来的影响,这会让他们觉得有意义。这不仅能保持关系的温暖,还鼓励他们在未来帮助更多的人。

你可以做的具体事项:

  • 如果有人为你推荐了工作或其他联系,请更新他们结果,即使没有成功。

  • 分享里程碑或成就,特别是如果他们的建议对其有所贡献。像“感谢你对[具体细节]的建议,我成功地[具体成就]”这样的简短信息就足够了!

第 6 步:优雅地退出

礼貌往往会有很大帮助。这是体面之举。

自从我开始在网上写关于我的经验后,LinkedIn 上向我寻求帮助的人数激增。全职工作、我的博客和个人承诺使得我的时间有限。

我们常听说时间是我们最宝贵的资产,今年我比以往任何时候都更能体会到这一点。时间是我生活中最重视的东西,这对你寻找的其他专业人士来说可能也是如此。

当我确实分配时间来帮助他人时,这是一个有意识的决定,通常意味着其他任务被搁置。大多数情况下,我选择帮助的人都很友好,一直到他们得到想要的东西,那时这种友好突然消失得无影无踪。

这不是期望不断的感激,而是保持基本的人性尊严。记住,网络拓展是关于培养和维护真实关系。

外面的世界很小。匆忙离开或不经过深思可能会回来伤害我们。因此,始终保持高尚的离开方式是好的,这为未来的机会留下了大门。

你可以做的事情:

  • 如果有人将你与另一位专业人士联系起来,无论结果如何,都要回头感谢原始联系人。TopResume 于 2021 年进行的一项研究显示,68% 的招聘专业人士承认,面试者的感谢信会影响他们的决定。

  • 如果有人花时间回答了你的问题,请花时间回复他们,并且要准时。

保持尊重,重视他人的时间,并且不要忘记表达感激。

成功时回馈

网络圈:从学生到导师 — 作者图像(Dall.E)

当你也成为其他人的资源时,网络圈就完成了。

一旦你成功了,务必记住你的出发点。我见过许多人由于其他专业人士的支持和帮助而取得成功,但没有将同样的礼貌延伸给新的参与者。

在 Medium 上写作是我回馈社区的方式,为了感谢社区在某种程度上给予我的帮助。这是我回报在我最困难时给予我善意的方式。

网络的美妙之处在于共同成长——当你帮助他人成长时,你也在他们的职业旅程中巩固了自己的足迹。他们不会忘记你,你也很可能不会忘记他们。

你怎么做呢?

  • 提供指导年轻的专业人士或来自你母校的学生。

  • 在你的网络或 LinkedIn 上分享职位空缺或机会。

  • 主持或参与网络研讨会、播客,或撰写文章分享你的专业知识和经验。

看到有人因为你的小帮助而成功,这种满足感是无可比拟的。

那么,我是怎么被录用的呢?

我实施了我在这篇文章中提到的大多数步骤。

  1. 参加了一次会议,其中一位我感兴趣的公司的高级数据科学家正在演讲。

  2. 在被拒绝后,我在 LinkedIn 上联系了他。我保持了礼貌和专业。

  3. 我问他是否可以进一步讨论我的申请,但他把我引导到了招聘人员那里。然后我优雅地结束了对话

  4. 与此同时,我被拒绝了申请过程。

  5. 十天后,他再次联系了我。他告诉我他们正在进行新一轮的招聘,并提出推荐我担任该职位。

  6. 我给他发送了我的简历,专门为那家公司量身定做

  7. 一周后,我已经获得了加入团队的邀请!

我最后没有加入这家公司,因为我随后收到了 Spotify 的录用通知,但正如你所见,除了我刚刚与你分享的内容,没有其他大的秘密。

我最初被拒绝的原因是因为我的简历没有通过筛选测试。造成这种情况的原因有很多:系统没有挑选到简历、招聘人员没有看到简历等等。

我有资格获得这份工作,这也是为什么网络在我这里奏效的原因,所以确保你也是如此。

如果我没有采取额外的步骤进行联系和网络,我就不会有机会通过筛选过程、面试并证明自己是值得的!

要进一步了解,请查看我关于如何破解当前数据科学职位市场的完整指南!

## 导航当前数据科学职位市场的指南

为什么现在很难找到数据科学的工作,以及你可以做些什么

[towardsdatascience.com

总结 — 打造你的网络游戏的步骤

  • 利用你现有的网络 — 触及朋友、家人、同学和同事。公开分享你的职业目标,寻求介绍或推荐。

  • 扩展你的网络 — 参加行业活动、网络研讨会和会议。与与会者和演讲者互动,并在 LinkedIn 上与他们建立联系。

  • 提出具体问题 — 联系时要直接明确。一般性的问题往往被忽视。做些研究,并根据对方的经验或当前角色提出有针对性的问题。

  • 保持职业礼仪 — 注意细节,例如正确拼写名字。当有人提供见解或帮助时,总是要表示感谢。注意他人的时间。

  • 提供更新 — 如果有人帮助了你,与你分享你的进展和里程碑。这样可以保持关系的温暖,并鼓励他们在必要时进一步帮助你。

  • 优雅地结束 — 当你结束互动时,要礼貌和尊重。记住,网络关系是关于建立真实的、长期的关系。

  • 传递善意 — 一旦你在职业生涯中取得了进展,记得帮助他人。分享知识、指导他人,甚至仅仅是传递工作机会,都可以对他人的旅程产生重大影响。

我为你准备了礼物 🎁!

注册我的新闻通讯 K's DataLadder,你将自动获得我的终极 SQL 备忘单,包含我在大科技公司工作中每天使用的所有查询 + 另一个神秘礼物!

我每周分享在科技行业担任数据科学家的经历,以及实用技巧、技能和故事,旨在帮助你提升自己 —— 因为没有人真正了解,直到他们身临其境!

如果你还没有这样做

再见啦!

posted @ 2024-10-12 19:56  绝不原创的飞龙  阅读(341)  评论(0)    收藏  举报