TowardsDataScience 2023 博客中文翻译(一百三十六)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

使用 Python 探索强化学习的第一步

原文:towardsdatascience.com/first-steps-in-the-world-of-reinforcement-learning-using-python-b843b76538e3

原始的 Python 实现,展示了如何在强化学习的基本世界之一——网格世界中找到最佳位置

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Eligijus Bujokas

·发表于 Towards Data Science ·15 分钟阅读·2023 年 1 月 13 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

网格世界矩阵;作者提供的照片

本文的目的是使用 Python 代码和注释介绍强化学习(以下简称RL)中的基本概念和定义。

这篇文章深受以下伟大强化学习课程的启发:www.coursera.org/learn/fundamentals-of-reinforcement-learning

理论见书¹:www.incompleteideas.net/book/RLbook2020.pdf

我所有的强化学习实验代码可以在我的 Gitlab 仓库中查看:github.com/Eligijus112/rl-snake-game

网格世界问题是强化学习中的一个经典问题,我们希望为代理创建一个优化的策略以穿越网格。

网格是一个方形的单元格矩阵,代理可以在每个单元格中向任意四个方向(上、下、左、右)移动。代理每移动一步会获得-1 的奖励,若达到目标单元格则获得+10 的奖励。奖励的数值是任意的,可以由用户定义。

在强化学习框架中,代理被正式定义为做出采取何种行动的决策的组件。代理在具体的时间步骤中采取行动。

在网格世界设置中,整个动作集合由以下集合定义:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

动作集合

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

动作集合

无论我们的代理处于何处,它只能向左、向右、向上或向下移动。现在让我们定义并可视化我们的网格世界:

def array_index_to_matplot_coords(i: int, j: int, n_cols: int) -> Tuple[int, int]:
    """Converts an array index to a matplot coordinate"""
    x = j
    y = n_cols - i - 1
    return x, y

def plot_matrix(
    M: np.array, 
    goal_coords: list = [],
    img_width: int = 5, 
    img_height: int = 5, 
    title: str = None,
    annotate_goal: bool = True
    ) -> None: 
    """
    Plots a matrix as an image.
    """
    height, width = M.shape

    fig = plt.figure(figsize=(img_width, img_width))
    ax = fig.add_subplot(111, aspect='equal')

    for y in range(height):
        for x in range(width):
            # By default, the (0, 0) coordinate in matplotlib is the bottom left corner,
            # so we need to invert the y coordinate to plot the matrix correctly
            matplot_x, matplot_y = array_index_to_matplot_coords(x, y, height)

            # If there is a tuple of (x, y) in the goal_coords list, we color the cell gray 
            if (x, y) in goal_coords:
                ax.add_patch(matplotlib.patches.Rectangle((matplot_x - 0.5, matplot_y - 0.5), 1, 1, facecolor='gray'))
                if annotate_goal:
                    ax.annotate(str(M[x][y]), xy=(matplot_x, matplot_y), ha='center', va='center')
            else: 
                ax.annotate(str(M[x][y]), xy=(matplot_x, matplot_y), ha='center', va='center')

    offset = .5    
    ax.set_xlim(-offset, width - offset)
    ax.set_ylim(-offset, height - offset)

    ax.hlines(y=np.arange(height+1)- offset, xmin=-offset, xmax=width-offset)
    ax.vlines(x=np.arange(width+1) - offset, ymin=-offset, ymax=height-offset)

    plt.title(title)
    plt.show()
# Importing the array library
import numpy as np

# Defining the number of blocks of a n x n grid 
n = 7

# Defining the value for the hole and the goal
goal = 10
step = -1

# Initiating an empty dataframe of size n x n
R = np.ones((n,n))

# Defining the coordinates of the goal
goal_coords = [(0, n-1), (n-1, 0), (0, 0), (n-1, n-1), (n // 2, n // 2)]

# Adding the goal values to the center and the corners
for goal_coord in goal_coords:
    R[goal_coord[1], goal_coord[0]] = goal

# Every other step is -1
R[R == 1] = step

# Converting the G matrix to int 
R = R.astype(int)

# Ploting
plot_matrix(R, goal_cords, title='Gridworld')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

网格世界;作者照片

在上述示例中,我们定义了第一个所需的矩阵——R矩阵或奖励矩阵。目标位于网格世界的中心和角落。当代理进入其中一个单元格时,它会获得该单元格的奖励值。

让我们定义另一个关键矩阵——状态矩阵S

S = np.arange(0, n*n).reshape(n, n)

plot_matrix(S, goal_coords, title='State space')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态空间;作者照片

在我们定义的网格世界中,总共有49 个状态,代理可以处于其中的任何一个状态。每个状态可以通过矩阵中的整数来识别。

假设我们的代理在状态 17 并向下移动。该动作值**,表示为 q,**是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态 17 中动作‘down’的动作值

动作值为 10,因为网格 24 的奖励等于 10。因此,当我们使用动作值时,需要注意网格S索引和奖励矩阵G。可以很容易猜到,从同一状态向右移动的奖励是-1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态 17 中动作‘right’的动作值

一般来说,函数 q,称为动作值,将一个数字映射到状态—动作对:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

动作值函数

数字越高,对代理的“奖励”就越大,因此,代理总是希望采取最大化****当前状态下的 q的动作。

到目前为止,我们已经定义了矩阵R(奖励)和S(状态)。另一个关键矩阵是状态值矩阵VV矩阵的维度与 S 和 G 矩阵相同,每个V矩阵中的元素评估给定状态的“好坏”。这里的“好坏”指的是方程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态 s 的值[1]

我们可以将上述方程读作:

给定策略 pi 的状态 s 的值等于在时间步 t 给定状态为 s 时的期望回报。

我们计算所有状态的上述值并将其存储在矩阵V中。我们在这里引入了新的变量,所以让我们定义它们。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

时间步 t 的总回报[1]

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

折扣因子[1]

K索引称为终端状态,其中代理达到网格世界中的任意目标。换句话说,每个状态中 G 的值表示从给定状态开始朝向目标的代理路径的折扣奖励总和。值越大,状态越受欢迎。

状态方程中的 pi 项称为策略,是采取状态 s 中某一动作的概率:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

策略

让我们初始化初始值矩阵V

# Initiating the empty Value function 
V = np.zeros((n, n))

plot_matrix(V, goal_coords, title='Value function')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

初始状态值矩阵;作者照片

由于我们尚未探索我们创建的网格世界,因此所有状态的回报都是 0。

我们需要的最后一个矩阵是策略矩阵P

def plot_policy_matrix(P: dict, S:np.array, goal_coords: list = [], img_width: int = 5, img_height: int = 5, title: str = None) -> None: 
    """ 
    Plots the policy matrix out of the dictionary provided; The dictionary values are used to draw the arrows 
    """
    height, width = S.shape

    fig = plt.figure(figsize=(img_width, img_width))
    ax = fig.add_subplot(111, aspect='equal')
    for y in range(height):
        for x in range(width):
            matplot_x, matplot_y = array_index_to_matplot_coords(x, y, height)

            # If there is a tuple of (x, y) in the goal_coords list, we color the cell gray 
            if (x, y) in goal_coords:
                ax.add_patch(matplotlib.patches.Rectangle((matplot_x - 0.5, matplot_y - 0.5), 1, 1, facecolor='gray'))

            else:
                # Adding the arrows to the plot
                if 'up' in P[S[x, y]]:
                    plt.arrow(matplot_x, matplot_y, 0, 0.3, head_width = 0.05, head_length = 0.05)
                if 'down' in P[S[x, y]]:
                    plt.arrow(matplot_x, matplot_y, 0, -0.3, head_width = 0.05, head_length = 0.05)
                if 'left' in P[S[x, y]]:
                    plt.arrow(matplot_x, matplot_y, -0.3, 0, head_width = 0.05, head_length = 0.05)
                if 'right' in P[S[x, y]]:
                    plt.arrow(matplot_x, matplot_y, 0.3, 0, head_width = 0.05, head_length = 0.05)

    offset = .5    
    ax.set_xlim(-offset, width - offset)
    ax.set_ylim(-offset, height - offset)

    ax.hlines(y=np.arange(height+1)- offset, xmin=-offset, xmax=width-offset)
    ax.vlines(x=np.arange(width+1) - offset, ymin=-offset, ymax=height-offset)

    plt.title(title)

# Saving all the unique states to a vector 
states = np.unique(S)

# Dictionary to hold each action for a given state
P = {}
for s in states: 
    s_dict = {}

    # Checking which index is the current state in the S matrix 
    s_index = np.where(S == s)

    # If the state is in the top left corner, we can only move right and down
    if s_index == (0, 0):
        s_dict['right'] = 0.5
        s_dict['down'] = 0.5

    # If the state is in the top right corner, we can only move left and down
    elif s_index == (0, n - 1):
        s_dict['left'] = 0.5
        s_dict['down'] = 0.5

    # If the state is in the bottom left corner, we can only move right and up
    elif s_index == (n - 1, 0):
        s_dict['right'] = 0.5
        s_dict['up'] = 0.5

    # If the state is in the bottom right corner, we can only move left and up
    elif s_index == (n - 1, n - 1):
        s_dict['left'] = 0.5
        s_dict['up'] = 0.5

    # If the state is in the first row, we can only move left, right, and down
    elif s_index[0] == 0:
        s_dict['left'] = 0.333
        s_dict['right'] = 0.333
        s_dict['down'] = 0.333

    # If the state is in the last row, we can only move left, right, and up
    elif s_index[0] == n - 1:
        s_dict['left'] =  0.333
        s_dict['right'] = 0.333
        s_dict['up'] = 0.333

    # If the state is in the first column, we can only move up, down, and right
    elif s_index[1] == 0:
        s_dict['up'] = 0.333
        s_dict['down'] = 0.333
        s_dict['right'] = 0.333

    # If the state is in the last column, we can only move up, down, and left
    elif s_index[1] == n - 1:
        s_dict['up'] = 0.333
        s_dict['down'] = 0.333
        s_dict['left'] = 0.333

    # If the state is in the middle, we can move in all directions
    else:
        s_dict['up'] = 0.25
        s_dict['down'] = 0.25
        s_dict['left'] = 0.25
        s_dict['right'] = 0.25

    # Saving the current states trasition probabilities
    P[s] = s_dict
# Drawing a plot for the policy matrix with arrows; In one cell there can be the maximum of 4 arrows each indicating the action an agent can take 
plot_policy_matrix(P, S, goal_coords, title='Policy matrix')

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

初始策略矩阵;作者照片

网格中的每个箭头代表代理可以采取的可用动作。初始矩阵中的概率是均匀的,目标状态中没有可用的移动。

拥有R, P, SV矩阵后,我们可以最终开始计算我们 RL 问题的答案。但我们还需定义 RL 目标。

RL 算法的目标是使代理找到最优策略 P,以最大化每个状态的回报。

另一种表述是目标是计算矩阵 V 中的最优状态值。

假设我们有一个 5 乘 5 的网格,目标在中央:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例网格世界;作者照片

为了建立直观理解,我已经计算了最佳值和策略。我们将在本文章的下一部分找到如何实现,但现在让我们解释以下VP矩阵:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

已解决的值和策略矩阵;作者照片

记住,V 矩阵中的每个值是总的累计折扣奖励。因此,我们的代理将始终希望前往具有最高值的状态。用数学表达就是,在每个状态下,代理将根据以下方程选择下一个状态:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个状态的最佳选择 [1]

在每个状态下,我们将选择一个动作,使我们进入状态s prime,在这里**r + gamma * (new state value)**是最高的。

更简单的直观理解是,我们可以列出当前状态下所有可用的动作,检查哪个可用状态具有最高的V(s)值,然后前往那里。从上面的矩阵中,我们可以看到,例如,状态 8 有两个最佳选择——。这是因为这些动作将使代理进入同样好的状态。因此,拥有V矩阵后,我们将始终推断出 P 矩阵。

现在,如何从全零的 V 矩阵转换为具有值的矩阵?我们需要为每个状态定义贝尔曼方程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态 s 的贝尔曼方程与策略 pi [1]

上面的方程令人望而生畏,且具有递归特性。对于网格世界示例,我们可以简化方程,并将其写出,而无需中间的条件概率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

简化方程 [1]

我们可以这样做,因为当我们在状态 s 中执行一个动作时,我们可以保证只会进入一个下一个状态。

在已解决的矩阵示例中,回顾一下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态 0 的值

这意味着从位置 0 开始,我们的代理在长期内将累计 -6.07 的总奖励。为了估计这一点并将递归公式转换为可以通过简单循环评估的公式,我们将使用以下算法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

价值迭代算法 [1]

我们将简化网格世界问题算法的中间部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

价值迭代算法简化;作者拍摄

现在让我们将一切转到 Python 代码中。

def get_next_state(a: str, s: int, S: np.array): 
    """ 
    Function that returns the next state's coordinates given an action and a state 
    """
    # Getting the current indexes 
    s_index = np.where(S == s)
    s_row = s_index[0][0]
    s_col = s_index[1][0]

    # Defining the indexes of the next state
    next_row = s_row 
    next_col = s_col

    if a == 'up':
        next_row = s_row - 1
        next_col = s_col
    elif a == 'down':
        next_row = s_row + 1
        next_col = s_col
    elif a == 'left':
        next_row = s_row
        next_col = s_col - 1
    elif a == 'right':
        next_row = s_row
        next_col = s_col + 1

    return next_row, next_col
def bellman_value(
    s: int, 
    S: np.array, 
    P: dict, 
    G: np.array, 
    V: np.array, 
    gamma: float = 0.9
    ) -> Tuple: 
    """
    Calculates the Belman equation value for the given state
    """
    # Extracting all the available actions for the given state
    actions = P[s]

    # Placeholder to hold the sum 
    sum = 0
    for action in actions: 
        # Extracting the probability of the given action 
        prob = actions[action]

        # Getting the next states indexes
        next_row, next_col = get_next_state(action, s, S)

        # Extracting the expected reward 
        reward = G[next_row, next_col]

        # Extracting the value of the next state
        value_prime = V[next_row, next_col]

        # Adding to the sum 
        sum += prob * (reward + gamma * value_prime)

    return sum

上述函数找到状态 s 的贝尔曼方程值。

def get_max_return(s: int, S: np.array, P: dict, G: np.array, V: np.array, gamma: float = 0.9) -> Tuple:
    """
    Returns the best action and the Bellman's value for the given state
    """
    # Extracting all the available actions for the given state
    actions = P[s]

    # Placeholder to hold the best action and the max return 
    best_action = None
    max_return = -np.inf

    for action in actions: 
        # Getting the probability of the action 
        prob = actions[action]

        # Getting the next states indexes
        next_row, next_col = get_next_state(action, s, S)

        # Extracting the expected reward 
        reward = G[next_row, next_col]

        # Extracting the value of the next state
        value_prime = V[next_row, next_col]

        # Calculating the return 
        _return = prob * (reward + gamma * value_prime)

        # Checking if the return is greater than the current max return
        if _return > max_return:
            best_action = action
            max_return = _return

    return best_action, max_return

def update_value(s, S, P, G, V, gamma) -> float:
    """
    Updates the value function for the given state
    """
    # Getting the indexes of s in S 
    s_index = np.where(S == s)
    s_row = s_index[0][0]
    s_col = s_index[1][0]

    # Getting the best action and the Bellman's value 
    _, max_return = get_max_return(s, S, P, G, V, gamma)

    # Rounding up the bellman value
    max_return = np.round(max_return, 2)

    # Updating the value function with a rounded value
    V[s_row, s_col] = max_return

    return max_return

def value_iteration(
    S: np.array, 
    P: np.array, 
    G: np.array, 
    V: np.array, 
    gamma: float = 0.9, 
    epsilon: float = 0.0001,
    n_iter: int = None 
    ) -> None: 
    """
    Function that performs the value iteration algorithm

    The function updates the V matrix inplace 
    """
    # Iteration tracker 
    iteration = 0

    # Iterating until the difference between the value functions is less than epsilon 
    iterate = True
    while iterate: 
        # Placeholder for the maximum difference between the value functions 
        delta = 0

        # Updating the iteration tracker
        iteration += 1 
        # Iterating over the states 
        for s in S.flatten():
            # Getting the indexes of s in S 
            s_index = np.where(S == s)
            s_row = s_index[0][0]
            s_col = s_index[1][0]

            # Saving the current value for the state
            v_init = V[s_row, s_col].copy()

            # Updating the value function
            v_new = update_value(s, S, P, G, V, gamma)

            # Updating the delta 
            delta = np.max([delta, np.abs(v_new - v_init)])

            if (delta < epsilon) and (n_iter is None): 
                iterate = False
                break

        if (n_iter is not None) and (iteration >= n_iter):
            iterate = False

    # Printing the iteration tracker
    print(f"Converged in {iteration} iterations")

    return None

上述代码块实现了价值迭代算法,用于寻找最佳(或接近最佳的) V 矩阵。

def update_policy(S, P, V): 
    """
    Function that updates the policy given the value function 
    """
    # Iterating over the states 
    for s in S.flatten(): 
        # Listing all the actions 
        actions = P[s]

        # For each available action, getting the Bellman's value
        values = {}
        for action in actions.keys():
            # Getting the next state indexes
            next_row, next_col = get_next_state(action, s, S)

            # Saving the value function of that nex t state
            values[action] = V[next_row, next_col]

        # Extracting the maximum key value of the values dictionary 
        max_value = max(values.values())        

        # Leaving the keys that are equal to the maximum value
        best_actions = [key for key in values if values[key] == max_value]

        # Getting the length of the dictionary 
        length = len(values)

        # Creating the final dictionary with all the best actions in it 
        p_star = {}
        for action in best_actions:
            p_star[action] = 1/length

        # Updating the policy 
        P[s] = p_star

我们现在有了所有理论和代码,开始评估网格世界中的所有状态。回顾一下,我们的初始网格世界如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态空间;作者拍摄

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

网格世界;作者拍摄

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

初始值和策略矩阵;作者拍摄

现在让我们更新一个状态——第一个或 s = 1

update_value(1, S, P, G, V, gamma=0.9)
update_policy(S, P, V)

价值矩阵和策略矩阵现在看起来如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个状态的价值迭代;作者拍摄

在状态 2 或 8 中,最佳策略是移动到状态 1,因为 0 < 2.66,因此状态 1 比其邻居更有价值。

现在让我们更新状态 3,看看会发生什么:

update_value(3, S, P, G, V, gamma=0.9)
update_policy(S, P, V)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

更新第三状态;作者拍摄

在状态 3 中的值为 -1,因此,目前在我们的网格世界中,代理会倾向于避免这个状态,相比于其邻居。

价值迭代算法以与上述相同的方式工作,只是针对所有状态(在我们的案例中——从状态 0 到 48)。要实现它,请使用以下代码:

value_iteration(S, P, G, V, epsilon=10**-16)
update_policy(S, P, V)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

已解决的网格世界;作者拍摄

代理可以从任何非终结状态开始,并沿着策略矩阵中的箭头移动。如果同一状态中有两个或更多箭头,我们可以以相同的概率移动到箭头指向的每一个状态。

总结一下,在一个简单的强化学习问题中,我们有 4 个主要矩阵:

  • 奖励矩阵 R

  • 状态值函数 V

  • 策略矩阵 P

  • 状态矩阵 S

此外,我们需要一个有限的动作集合 A

为了理论上评估每个状态,我们使用贝尔曼方程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

状态 s 的贝尔曼方程与策略 pi

为了实际评估状态值,我们使用价值迭代算法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

价值迭代算法简化;作者拍摄

强化学习任务的目标是找到我们的代理可以遵循的最佳策略。

随意使用代码并在此处进行调整:github.com/Eligijus112/rl-snake-game

祝学习愉快!

[1]

作者:理查德·S·萨顿,安德鲁·G·巴托

年份:2018

标题:强化学习:导论

链接:http://archive.ics.uci.edu/ml

FitBot — 一款健身聊天机器人代理

原文:towardsdatascience.com/fitbot-a-fitness-chatbot-agent-dca471710775?source=collection_archive---------3-----------------------#2023-07-28

如何创建一个利用 OpenAI 函数调用的聊天机器人代理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Solano Todeschini

·

关注 发布于 数据科学前沿 ·13 分钟阅读·2023 年 7 月 28 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 Gary Butterfield 提供,刊登于 Unsplash

介绍

在健康意识居于前沿的时代,追求平衡生活已成为普遍的愿望,营养无疑是核心支柱。

然而,饮食计划的复杂性和大量的营养数据常常成为我们实现这种平衡的障碍。一个常见的情况是糖尿病患者,他们需要持续和准确的营养指导以有效管理血糖水平。拥有一个个性化的营养助手会不会是一次变革性的体验?

在这种情况下,利用技术来辅助营养指导不仅是有益的,而且是必要的。通过将尖端的人工智能(AI)与全面的营养数据库整合,能够创建一个强大的工具,帮助个人在健康旅程中取得进展。

该项目的代码在这个 GitHub 仓库中: 链接**

项目概述

项目的核心涉及构建一个名为 FitBot 的聊天机器人,该机器人由 OpenAI 的功能驱动,并基于 ReAct(推理和行动)框架(见图 1)。

它旨在提供营养信息和建议,通过解读用户的饮食习惯并整合营养数据的 API 来实现这一目标。

技术方法

通过利用 ReAct 框架,FitBot 保持了对话的互动性质,并能对每条建议提供详细的解释。它还连接到外部营养数据库,确保提供准确且最新的饮食建议。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:ReAct 结合了推理(例如,链式思维提示)和行动。图片来源:链接

FitBot 的幕后

FitBot 将 OpenAI 的 GPT-4 能力与 ReAct 的动态处理结合起来,以理解饮食查询,建议合适的替代方案,并提供个性化建议。它在保持对话语气的同时,使营养建议变得易于获取和引人入胜。

FitBot 的独特之处在于其能够与外部营养数据库接口。这使得 FitBot 能够向用户提供准确和最新的信息,确保所提供的建议是可靠的,并基于准确的数据。

在接下来的部分,我们将深入探讨代码,看看 FitBot 的组件是如何开发和相互作用的,全面了解这个创新项目的内部工作。

构建 FitBot:代码解析

这个项目有四个主要脚本,用于处理数据并在用户界面中显示数据:

  1. fitness_agent.py:此文件包含 FitnessAgent 类,该类利用 OpenAI 功能实现 FitBot 所需的功能。

  2. chatbot.py:此文件包含 FitBot 用户界面的代码,使用 Gradio 库实现。

  3. agents.py:这个文件包含Agent类,用于处理与 OpenAI API 的对话。这段代码基于由James Briggs在 funkagent 库中开发的this script

  4. parser.py:这个文件包含将函数文档字符串解析为 OpenAI 函数描述的代码。

定义我们的功能

创建能够提供准确和有用的营养和健身建议的聊天机器人时,我们需要考虑什么信息对最终用户最有价值。这就是前面解释的功能实现背后的原因。

1.**get_nutritional_info**:这个功能对于任何以健身为导向的聊天机器人都至关重要。人们经常缺乏关于他们所吃食物的营养含量的清晰信息。通过使用Nutrition endpoint from API Ninjas来获取各种食物实时营养数据,我们可以帮助用户做出明智的饮食决策。返回的数据可以包括卡路里、蛋白质、碳水化合物、脂肪等详细信息,全面了解食物项的营养概况。

def get_nutritional_info(self, query: str) -> dict:
    """Fetches the nutritional information for a specific food item

    :param query: The food item to get nutritional info for
    :return: The nutritional information of the food item
    """
    api_url = 'https://api.api-ninjas.com/v1/nutrition?query={}'.format(query)
    response = requests.get(api_url, headers={'X-Api-Key': self.nut_api_key})

    if response.status_code == requests.codes.ok:
        return response.json()  # Use json instead of text for a more structured data
    else:
        return {"Error": response.status_code, "Message": response.text}

2.**calculate_bmr**:基础代谢率(BMR)是理解个体新陈代谢的关键指标。它是在休息时消耗的能量量,与一个人的年龄、体重、身高和性别密切相关。计算 BMR 的能力为聊天机器人提供了一个基准,帮助用户理解他们的身体即使没有任何身体活动也需要多少卡路里。

def calculate_bmr(weight: float, height: float, age: int, gender: str, equation: str = 'mifflin_st_jeor') -> float:
    """Calculates the Basal Metabolic Rate (BMR) for a person

    :param weight: The weight of the person in kg
    :param height: The height of the person in cm
    :param age: The age of the person in years
    :param gender: The gender of the person ('male' or 'female')
    :param equation: The equation to use for BMR calculation ('harris_benedict' or 'mifflin_st_jeor')
    :return: The BMR of the person
    """
    if equation.lower() == 'mifflin_st_jeor':
        if gender.lower() == 'male':
            return (10 * weight) + (6.25 * height) - (5 * age) + 5
        else:  # 'female'
            return (10 * weight) + (6.25 * height) - (5 * age) - 161
    else:  # 'harris_benedict'
        if gender.lower() == 'male':
            return 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age)
        else:  # 'female'
            return 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age)

3.**calculate_tdee**:了解一个人的总日能量消耗(TDEE)对于制定个性化的饮食或锻炼计划至关重要。TDEE 不仅考虑了 BMR,还考虑了日常活动和锻炼中消耗的卡路里。了解他们的 TDEE 可以帮助用户更有效地计划他们的饮食和锻炼计划,以维持、减少或增加体重。

def calculate_tdee(bmr: float, activity_level: str) -> float:
    """Calculates the Total Daily Energy Expenditure (TDEE) for a person

    :param bmr: The BMR of the person
    :param activity_level: The activity level of the person
    ('sedentary', 'lightly_active', 'moderately_active', 'very_active', 'super_active')
    :return: The TDEE of the person
    """
    activity_factors = {
        'sedentary': 1.2,
        'lightly_active': 1.375,
        'moderately_active': 1.55,
        'very_active': 1.725,
        'super_active': 1.9,
    }
    return bmr * activity_factors.get(activity_level, 1)

4.**calculate_ibw**:了解理想体重(IBW)可以为用户提供一个被认为对他们的身高和性别健康的体重目标。虽然 IBW 不是一个完美的衡量标准(它没有考虑肌肉质量等因素),但它确实为用户提供了一个关于他们的体重应该达到的大致想法,以达到最佳健康。

def calculate_ibw(height: float, gender: str) -> float:
    """Calculates the Ideal Body Weight (IBW)

    :param height: The height of the person in inches
    :param gender: The gender of the person ("male" or "female")
    :return: The Ideal Body Weight in kg
    """
    if gender.lower() == 'male':
        if height <= 60:  # 5 feet = 60 inches
            return 50
        else:
            return 50 + 2.3 * (height - 60)
    elif gender.lower() == 'female':
        if height <= 60:
            return 45.5
        else:
            return 45.5 + 2.3 * (height - 60)
    else:
        raise ValueError("Invalid gender. Expected 'male' or 'female'.")

4. **calculate_bmi**:身体质量指数(BMI)是使用一个人的身高和体重进行简单计算的。公式是 BMI = kg/m²,其中 kg 是一个人的体重(单位:千克),m² 是他们的身高的平方(单位:米)。BMI 并不直接测量体脂肪,但研究表明 BMI 与更直接的体脂肪测量指标有中等相关性。它提供了一个有用的标准来理解一个人是偏瘦、健康、超重还是肥胖。

def calculate_bmi(weight: float, height: float) -> float:
    """Calculates the Body Mass Index (BMI) for a person

    :param weight: The weight of the person in kg
    :param height: The height of the person in cm
    :return: The BMI of the person
    """
    height_meters = height / 100  # convert cm to meters
    bmi = weight / (height_meters ** 2)
    return round(bmi, 2)  # round to 2 decimal places for readability

构建代理:包装功能

在建立了必要的功能后,我们的下一步是将这些功能集成到我们的聊天机器人代理中。这种封装使得机器人能够利用这些功能,并根据用户查询提供相关、准确的回应。

以下是如何创建代理的方法:

# Instantiate the agent
fitness_agent = FitnessAgent(openai_api_key, nut_api_key)

你可以查看它所增强的功能:

# You can view the processed function instructions
print(fitness_agent.functions)

输出:

[
   {
      "name":"get_nutritional_info",
      "description":"Fetches the nutritional information for a specific food item",
      "parameters":{
         "type":"object",
         "properties":{
            "query":{
               "description":"The food item to get nutritional info for",
               "type":"string"
            }
         }
      },
      "required":[
         "query"
      ]
   },
   {
      "name":"calculate_bmr",
      "description":"Calculates the Basal Metabolic Rate (BMR) for a person",
      "parameters":{
         "type":"object",
         "properties":{
            "weight":{
               "description":"The weight of the person in kg",
               "type":"number"
            },
            "height":{
               "description":"The height of the person in cm",
               "type":"number"
            },
            "age":{
               "description":"The age of the person in years",
               "type":"integer"
            },
            "gender":{
               "description":"The gender of the person ('male' or 'female')",
               "type":"string"
            },
            "equation":{
               "description":"The equation to use for BMR calculation ('harris_benedict' or 'mifflin_st_jeor')",
               "type":"string"
            }
         }
      },
      "required":[
         "weight",
         "height",
         "age",
         "gender",
         "equation"
      ]
   },
   {
      "name":"calculate_tdee",
      "description":"Calculates the Total Daily Energy Expenditure (TDEE) for a person",
      "parameters":{
         "type":"object",
         "properties":{
            "bmr":{
               "description":"The BMR of the person",
               "type":"number"
            },
            "activity_level":{
               "description":"The activity level of the person",
               "type":"string"
            }
         }
      },
      "required":[
         "bmr",
         "activity_level"
      ]
   }
]

与代理互动:用户对话

在将定义的函数封装到 FitnessAgent 中之后,我们现在可以模拟与我们的机器人对话。通过使用 FitnessAgent 类的 ask() 方法,我们可以轻松地将用户查询输入到我们的机器人中,并打印出生成的响应。

例如,我们可以询问机器人一些关于常见食物的营养信息:

# Define a question
user_input = "What is the nutritional value of a banana?"

# Get raw chat response
response = fitness_agent.ask(user_input)

# Print final response
print(response['choices'][0]['message']['content'])

输出:

A 100-gram serving of banana typically contains:

- Calories: 89.4
- Total Fat: 0.3 grams, of which Saturated Fat is 0.1 grams
- Protein: 1.1 grams
- Sodium: 1 milligram
- Potassium: 22 milligrams
- Cholesterol: 0 milligram
- Total Carbohydrates: 23.2 grams, of which Dietary Fiber is 2.6 grams and Sugars are 12.3 grams

These values may vary based on the exact size and ripeness of the banana. Also note, bananas are a good source of dietary potassium and vitamin C.

在这个示例中,FitBot 有效地使用了 get_nutritional_info() 函数来获取并显示香蕉的营养价值。

FitBot 也可以处理更复杂的请求。例如,如果提供所需的数据,它可以计算一个人的总每日能量消耗 (TDEE):

# Define a question
user_input = "What is the TDEE of a 30-year-old man, who is 180 cm tall, weighs 80 kg, and exercises 3 times a week?"

# Get raw chat response
response = fitness_agent.ask(user_input)

# Print final response
print(response['choices'][0]['message']['content'])

输出:

The Total Daily Energy Expenditure (TDEE) of a 30-year-old man who is 180 cm tall, weighs 80 kg, and exercises 3 times a week with "moderate" exercise according to the Harris-Benedict equation would be approximately 2574 calories a day. This is a rough estimate and individual results could vary based on multiple factors like metabolism speed, specific physical activity, and more. 

Here's a simple breakdown of the process:

1\. Calculating Basal Metabolic Rate (BMR) - This is the number of calories a person would burn just by existing, without any activity. 

For men, the Harris-Benedict equation to calculate BMR is 
BMR = 88.362 + (13.397 * weight in kg) + (4.799 * height in cm) - (5.677 * age in years)
For the given values, the BMR comes to around 1783 calories.

2\. Adjusting for Activity - The BMR can thus be adjusted for physical activity level using the following multipliers:

- Sedentary (little or no exercise): BMR * 1.2
- Lightly active (light exercise/sports 1-3 days/week): BMR * 1.375
- Moderately active (moderate exercise/sports 3-5 days/week): BMR * 1.55
- Very active (hard exercise/sports 6-7 days a week): BMR * 1.725
- Super active (very hard exercise/physical job & exercise 2x/day): BMR * 1.9

For someone who exercises 3 times a week, we could take the multiplier to be at 1.55 (Moderately active). 

Hence, TDEE = BMR * Activity Multiplier = 1783 * 1.55 = approximately 2764 calories.

Note: The actual total caloric needs may be slightly higher or lower as this calculation doesn't take into account muscle to fat ratio, specific types of activities, etc.

让我们将这个输出与基于 GPT-4 的原始 ChatGPT 进行比较:

TDEE, or Total Daily Energy Expenditure, represents the total number of calories a person needs to consume in a day to maintain their current weight, considering their physical activity level.

We first need to calculate the Basal Metabolic Rate (BMR), which is the amount of energy expended while at rest. There are several equations to calculate BMR, one of the commonly used ones is the Mifflin-St. Jeor equation:

For men: BMR = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(y) + 5

So, for your case:

BMR = 10 * 80kg + 6.25 * 180cm - 5 * 30y + 5
BMR = 800 + 1125 - 150 + 5 = 1780 kcal/day

This is the amount of calories needed to keep the body functioning while at rest.

To include physical activity in the equation, we multiply the BMR by the Physical Activity Level (PAL). For someone who exercises 3 times a week, a common PAL is 1.375 (moderate exercise).

So, TDEE = BMR * PAL

TDEE = 1780 kcal/day * 1.375 = 2447.5 kcal/day

So, a 30-year-old man who is 180 cm tall, weighs 80 kg, and exercises 3 times a week would need approximately 2448 calories per day to maintain his current weight. Please note this is just an estimate; actual caloric needs can vary based on many factors, including genetics and body composition. Always consult a healthcare provider or a registered dietitian for personalized advice.

将聊天机器人与用户界面集成

现在我们已经构建了健身代理并为其配备了有用的功能,我们希望将其展示在一个易于使用的界面中。

为此,我们使用 Gradio,这是一个 Python 库,允许我们迅速便捷地创建可共享的基于网页的用户界面。在本节中,我们将带你了解如何将聊天机器人与 Gradio 用户界面集成。

这是我们界面的整体结构:

def main():

    openai_api_key = gr.components.Textbox(
        lines=1,
        label="Enter OpenAI API Key",
        type="password",
    )

    nut_api_key = gr.components.Textbox(
        lines=1,
        label="Enter Nutrition API Key",
        type="password",
    )

    question = gr.components.Textbox(
        lines=3,
        label="Enter your message",
    )

    output_history = gr.outputs.HTML(
        label="Updated Conversation",
    )

    inputs = [
        openai_api_key,
        nut_api_key,
        question,
    ]

    iface = gr.Interface(
        fn=partial(get_response),
        inputs=inputs,
        outputs=[output_history],
        title="Fitness Agent",
        description="A simple chatbot using a Fitness Agent and Gradio with conversation history",
        allow_flagging=False,
    )

    iface.launch()

if __name__ == "__main__":
    main()

这是我们的 main 函数,脚本的入口点。我们首先创建文本框,让用户输入他们的 OpenAI API 密钥和 Nutrition API 密钥。这些密钥设置为 password 类型以隐藏输入。接着,我们提供一个文本框供用户提问。机器人的回应将以 HTML 格式显示在标记为“Updated Conversation”的区域。

输入和输出随后传递给 Gradio 界面,脚本运行时启动该界面。

get_response 函数与健身代理互动:

def get_response(openai_api_key, nut_api_key, user_input, action=None):
    set_openai_api_key(openai_api_key)
    set_nut_api_key(nut_api_key)

    fitness_agent = FitnessAgent(openai_api_key, nut_api_key)

    # Get raw chat response
    fitness_agent.ask(user_input)

    memory = fitness_agent.agent.chat_history

    # Iterate through messages in ChatMessageHistory and format the output
    updated_conversation = '<div style="background-color: hsl(30, 100%, 30%); color: white; padding: 5px; margin-bottom: 10px; text-align: center; font-size: 1.5em;">Chat History</div>'
    logger.info(memory)
    for i, message in enumerate(memory):
        if i != 0:
            if message['role'] == 'user':
                prefix = "User: "
                background_color = "hsl(0, 0%, 40%)"  # Dark grey background
                text_color = "hsl(0, 0%, 100%)"  # White text
            else:
                prefix = "Chatbot: "
                background_color = "hsl(0, 0%, 95%)"  # White background
                text_color = "hsl(0, 0%, 0%)"  # Black text
            updated_conversation += f'<div style="color: {text_color}; background-color: {background_color}; margin: 5px; padding: 5px;">{prefix}{message["content"]}</div>'
    return updated_conversation

get_response 中,我们使用 set_openai_api_keyset_nut_api_key 函数设置 OpenAI 和 Nutrition API 密钥,然后初始化我们的健身代理。接着,我们调用代理的 ask 方法,传入用户的问题,并存储对话历史记录。对话历史中的每条消息都被格式化为 HTML 字符串,并添加到 updated_conversation。这个 HTML 字符串被返回并显示在 Gradio 界面中。

结果界面的概览

在整合了必要的计算和对话逻辑,并将这些内容封装到一个视觉上令人愉悦的 Gradio 用户界面中之后,我们的 FitBot 已经准备好与用户互动了!

最终界面如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2:聊天机器人的 Gradio 用户界面。图片由作者提供。

在界面上,你会看到三个输入框,可以在其中输入 OpenAI 和 API Ninjas 营养端点的必要密钥,以及用户对健身代理的消息。

摘要

本文详细描述了 FitBot 的创建,这是一个使用 OpenAI 的 GPT-4 的综合健身代理,一个能够理解和响应复杂用户查询的强大 AI 模型。

我们从构建计算关键健康指标的函数开始,如基础代谢率(BMR)、每日总能量消耗(TDEE)和体重指数(BMI)。这些计算构成了健身代理提供准确和量身定制的健身和营养建议的基础。

接下来,我们集成了 API Ninjas 的营养端点。这使得健身代理能够访问并提供准确的营养信息,这是任何全面健身和饮食计划的关键组成部分。

我们展示了如何构建对话逻辑,使健身代理更加互动。它能够处理对话流程,使其能够回答各种用户查询,并有效地引导用户进行健身之旅。

最后,我们使用 Gradio 将所有这些功能封装到一个视觉上吸引人的用户界面中。结果是一个不仅智能而且用户友好的健身代理,提供清晰且全面的建议,格式易于理解。

概况*:在这篇文章中,我们构建了 FitBot,一个使用 OpenAI 的 GPT-4 的健身代理,能够提供个性化的健身和营养建议。我们实现了计算关键健康指标(BMR、TDEE、BMI)的功能,集成了一个营养 API,以获取准确的饮食信息,并通过 Gradio 封装了一个用户友好的界面。这个项目展示了 AI 在健康和健身领域的强大功能,简化了复杂的计算,提供了个性化建议,并通过吸引人的用户界面传递所有信息。*

感谢阅读!

五个免费且可靠的天气数据来源

原文:towardsdatascience.com/five-free-and-reliable-weather-data-sources-20b9ea6afac9

高质量数据以改善和增强你的建模过程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Anthony Baum

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 NOAA 提供,来自 Unsplash

天气以从明显到微妙和意外的方式影响人类决策。能源供应商需要数据来确保当电网上的成千上万台空调启动时,电力供应能够满足需求。纽约市的 Citibike 骑行需求在开始下雨时急剧下降。零售商甚至使用天气数据在季节性寒冷天气时向消费者推销感冒和流感药物。

大量的天气数据由政府和学术机构收集,这意味着在进行分析和建模时,你所需的数据通常是免费的。在美国尤其如此,因为由国家海洋和大气管理局 (NOAA) 收集的数据,包括国家天气服务 (NWS),是公共领域数据。

许多高质量的免费数据往往隐藏在难以找到的过时托管区域、FTP 服务器中,或者在 Google 的两页深处,背后有一大堆私人公司。例如,NOAA 的许多数据仅通过购物车流程提供,你可能需要在开始查找之前知道你想要的确切数据集。获取这些数据的难度意味着你最容易的选择通常是具有 API 和高搜索排名的私人数据提供商。这些来源可以很棒,但通常有严格的免费层级限制,如速率限制、历史时间限制、分辨率限制等。因此,为了帮助你寻找数据,我整理了五个高质量且可靠的免费数据来源。

国家环境信息中心 (NCEI)

数据集 | 气候数据在线 (CDO) | 国家气候数据中心 (NCDC) (noaa.gov)

NCEI 隶属于国家海洋和大气管理局(NOAA),提供了地球上最重要的环境数据档案之一。他们提供关于大气、沿海、海洋和地球物理参数的综合信息。你可以以各种格式下载历史天气模式、古气候学、太阳事件等数据。

如果你访问上述数据集链接,一个很好的起点是“遗留应用程序”下的全球小时数据。你将能够获取全球大多数主要机场天气站的每小时数据,以及美国的每个官方站点(主要但不总是机场)。如果你试图为特定地点增加数据,可以从最近的机场开始。

另一个有趣的数据集是风暴事件数据库,它不仅提供关于龙卷风和冰雹的信息,还记录了如佛罗里达的雪等异常事件,以及导致人员伤亡的气象相关事件,如雪崩。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

风暴报告数据,通过 NCEI 风暴事件数据库以原始形式提供。图片来源:NOAA/NWS 风暴预警中心

气候规范等长期数据可以在气候数据在线下找到,特别适用于基于季节性的项目。在同一部分,你还可以查看天气雷达数据,这是来自 NWS 雷达安装的反射率数据。如果你在寻找像超本地降雨这样的数据,雷达数据是最好的选择,而且无论好坏,都将在使其可用于代码分析方面给你带来重大技术挑战。

如前所述,NOAA 数据属于公共领域,因此在此收集的任何内容都可以用于任何目的,包括商业用途。

欧洲中期天气预报中心(ECMWF)

ECMWF Reanalysis v5 | ECMWF

ECMWF 是一个独立的组织,总部位于英国,由多个欧洲国家支持。它提供中期到长期的天气预报,并通过 ECMWF Web API 允许免费访问其数据集。他们的 ERA5 数据集对于气候研究特别有价值,提供了从 1979 年至今的详细大气、陆地表面和海洋波浪信息。

与 NOAA 观测数据不同,ERA5 是所谓的“再分析数据”。与观测数据相比,再分析数据本质上是模型插值的天气视图。对于 ERA5,数据是全球性的,分辨率为 0.25 度。每个数据点位置都有多个垂直层可用,因此你可以创建自 1979 年以来的任意时刻的大气垂直剖面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

全球东向风速,从 ERA5 数据中得出。来源:Copernicus 气候变化服务信息 2023

对我们这些数据人员来说,这些数据通过一个优秀的 API 提供,该 API 访问 ECMWF Copernicus 数据存储(CDS)。使用该服务,你需要注册并获取 API 密钥。使用 API 的一个专业技巧是下载 NetCDF 格式的数据,并使用 xarray Python 模块将其读取到 pandas 数据框中。

ECMWF 在这个列表中排名很高,因为它是一个高质量且受推崇的数据来源。然而,你必须小心理解他们提供的数据的使用条款,因为他们不像美国政府来源那样在公共领域下运营。在公开访问的内容中,有些项目受自身使用条款的限制。这些项目要么标明了谁控制该项目的权利,并在某些情况下提供了点击使用许可的链接,要么在图片说明中列出了使用限制。开放数据页面是一个很好的起点,提供了可用数据的摘要、交付方式和明确的许可要求链接。

国家气象局(NWS)

API Web 服务 (weather.gov)

如果你在美国,并且曾经在电视上或手机上看到过天气警报,那是 NWS 的一位气象预报员发布的。如果你需要访问当前数据,例如,你有一个实时运行的模型或仪表板,这就是你需要开始的地方。

该 API 提供了对包括温度、降水、预报和波高在内的当前条件的访问,涵盖了整个美国及其领土水域。全球范围内也可以获得更有限的当前条件报告。文档和访问详情可以在上面链接的 NWS 官方网站上找到。

与 NCEI 数据一样,这属于公共领域,可以自由用于任何目的,但要小心不要违反速率限制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

过去三天的天气观测数据。来源:NWS 洛杉矶,USC/市区 LA 站点的观测数据

Open-Meteo

免费开源天气 API | Open-Meteo.com

Open-Meteo 是一个合作项目,旨在提供开放、免费的天气数据访问。虽然它不是政府来源,但由于其致力于让气象数据免费获取,因此在这里列出。

他们提供了一个 API,提供对众多数据集的访问,包括预报、观测数据、历史数据和雷达图像。该 API 支持多种编程语言,使其成为将数据集成到应用程序中的一个好选择。

你可以将 API 用于个人和学术项目,但不能用于商业项目。

日本气象厅

日本气象厅 | 日本气候 (jma.go.jp)

日本气象厅(JMA)免费提供天气、气候和地震数据。虽然这些数据的细致程度和深度不及 NOAA 或 ECMWF 提供的数据,但却是获取日本本地天气和气候信息的最佳来源。

特别值得注意的是,JMA 的日环气象卫星是目前运行中最好的卫星之一,提供了极高质量的日本、东南亚、澳大利亚、新西兰以及太平洋岛屿的影像。

尽管 JMA 没有专用的 API,但包括观测、预报和警告在内的数据集以文本、XML 和图形格式等多种格式提供,可以在上面的链接或网站上相对容易找到。如果你深入挖掘,可能需要使用翻译工具,因为大部分信息是用日语写的。

希望这些资源对你有所帮助,你能找到你所需要的信息。祝你的项目好运!

你应该注意的五个数据泄露的隐藏原因

原文:towardsdatascience.com/five-hidden-causes-of-data-leakage-you-should-be-aware-of-e44df654f185

以及它们如何破坏机器学习模型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Donato Riccio

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Linh Pham 提供,来源于 Unsplash

数据泄露是一个隐蔽的问题,通常困扰机器学习模型。泄露这个术语指的是测试数据泄漏到训练集中。当模型在训练过程中接触到不应有的数据时,就会发生这种情况,导致过拟合和在未见过数据上的表现不佳。这就像用考试答案来训练学生一样——他们在特定的考试中表现出色,但在其他考试中表现就不那么好。机器学习的目标是创建能够泛化并在新的、未见过的数据上做出准确预测的模型。数据泄露破坏了这一目标,因此了解和准备应对它非常重要。在本文中,我们将深入探讨什么是数据泄露,它的潜在原因,以及如何通过使用 Python 和 scikit-learn 的实际示例以及研究中的案例来防止它。

数据泄露的后果

  • 过拟合。 数据泄露的一个重大后果是过拟合。过拟合发生在模型被训练得过于贴合训练数据,以至于无法对新数据进行泛化。当数据泄露发生时,模型在开发过程中使用的训练集和测试集上的准确率很高。然而,当模型被部署时,它的表现不会那么好,因为它无法将其分类规则泛化到未见过的数据。

  • 误导性的性能指标。 数据泄露还可能导致误导性的性能指标。模型可能会表现出高准确率,因为它在训练过程中看到了部分测试数据。因此,很难评估模型并理解其性能。

划分前的数据泄露

我们呈现的第一个案例是最简单的,但可能是最常见的:在训练/测试划分之前进行预处理。

你想使用 StandardScaler 来标准化数据,因此你加载数据集,进行标准化,创建训练集和测试集,然后运行模型。对吗?错了。

0.745

均值和标准差是在整列上计算的,因此它们包括了测试集中的信息。使用这些值进行标准化处理意味着测试数据正在泄漏到训练数据中。

解决方案:管道

0.73

在这个版本中,使用了管道来封装预处理步骤,然后仅在训练集上进行拟合和评估。在这种情况下,StandardScaler被用作预处理步骤,它通过减去均值并缩放到单位方差来标准化特征。当你调用fit方法时,sklearn 会分别标准化每个数据集。这确保了测试集不会用于指导预处理步骤,从而避免了数据泄露。

使用交叉验证时的数据泄露

第二个例子是一个非常常见的错误,通常被忽视。你的数据集是不平衡的,你已经了解过应该如何使用过采样来“修复”它。经过一些搜索,你发现了 SMOTE,这是一种使用最近邻生成新样本以平衡少数类的算法。我们将这个技术应用于名为 credit_g 的数据集,来自 PMLB 库。

数据集是不平衡的,类别之间的比例为 70/30。

ROC AUC score (baseline): 0.75 +/- 0.01

作为基线结果,我们展示了不应用任何变换的 AUC 分数。运行逻辑回归模型的平均 ROC AUC 分数为 0.75。

现在让我们应用 SMOTE。

1    700
0    700
Name: target, dtype: int64

ROC AUC score (with data leakage): 0.84 +/- 0.07

应用 SMOTE 后,你会很高兴地看到 AUC 分数从 0.75 提高到 0.84!然而,所有的光芒都不是金子:你刚刚造成了数据泄露。在上面的代码中,变换在运行交叉验证之前应用,这将训练和测试集分割到不同的折中。这是一个非常常见的场景,可能会误导初学者认为 SMOTE 提高了他们的模型性能。

现在让我们看一下修正后的代码,其中 SMOTE 在交叉验证划分之后应用。

ROC AUC score: 0.67 +/- 0.00

正确应用 SMOTE 实际上使模型更糟。

正如Samuele Mazzanti在他的文章你的数据集不平衡?什么都不要做!中强调的那样,过采样并不是处理不平衡数据集的必要手段。

时间序列中的数据泄露

时间序列数据具有独特的特性,使其与其他类型的数据不同,这可能导致在划分数据、准备特征和评估模型时出现特定的挑战。在这里,我们将详细阐述这些挑战,并建议最佳实践以最小化时间序列分析中的数据泄露。

不正确的训练-测试划分:在时间序列数据中,将数据集分为训练集和测试集时,必须保持观察的时间顺序。随机划分可能会引入数据泄漏,因为它可能将未来的信息包含在训练集中。为了避免这种情况,应使用基于时间的划分,确保训练集中的所有数据点都在测试集的数据点之前。你还可以使用时间序列交叉验证或前向验证等技术,以更准确地评估模型的性能。

特征工程:你应该避免使用在预测时无法获得的未来信息。例如,计算技术指标、滞后变量或滚动统计数据时,只应使用过去的数据,而不是未来的数据。为了在特征工程过程中防止数据泄漏,你可以使用诸如应用基于时间的窗口函数等技术,确保计算窗口只包含到预测时间为止的数据。这也适用于外部数据。有时,时间序列模型会结合可能包含未来信息的外部数据源。确保指标适当地滞后,以免提供未来的信息,并始终验证外部数据源是否与主要时间序列数据集保持相同的时间顺序。

图像数据中的数据泄漏

当处理医学数据时,通常会从同一患者那里获取多张图像。在这种情况下,你不能仅仅随机划分数据集来训练模型,因为你可能会不小心将同一人的图像放在训练集和测试集中。相反,你需要使用按受试者划分的方法。

那么,什么是按受试者划分?这意味着你将同一人的所有图像放在一起,要么在训练集中,要么在测试集中。这样,你的模型就不能通过从两个集合中的同一人的图像中学习而作弊。

有一项研究探讨了随机划分与按受试者划分之间的差异。他们在三个不同的数据集上进行测试,发现随机划分会导致测试准确度虚高,因为数据泄漏。另一方面,按受试者划分则能得到更准确的结果。使用的数据集如下:

AIIMS 数据集:包含来自 45 名受试者(22 名癌症患者和 23 名健康受试者)的 18,480 张健康和癌症乳腺组织的 2D OCT 图像。

Srinivasan 数据集:一个眼科学数据集,包括 3,231 张年龄相关性黄斑变性(AMD)、糖尿病性黄斑水肿(DME)和正常受试者的 2D OCT 图像,每类包括 15 名受试者。

Kermany 的数据集:一个大型开放获取的眼科数据集,包含来自 5,319 名患者的脉络膜新生血管(CNV)、糖尿病性黄斑水肿(DME)、视网膜上皮下层(drusen)和正常视网膜图像。该数据集有不同版本,图像数量、组织和训练与测试集之间的数据重叠有所不同。

结果不言自明。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不同分割策略的比较。来源。

模型通过 Matthews 相关系数进行评估,定义如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Matthews 相关系数。图像由作者提供。

正如你可以想象的那样,当我们随机分割数据时,得到的分数好得令人难以置信。这是因为来自同一个人的图像看起来非常相似,因此模型在识别训练数据中已经见过的人时更容易。在现实世界中,我们需要能够可靠识别新患者疾病的模型。

数据泄露是一个常见的问题,即使是最熟练的数据科学家在构建机器学习模型时也会受到影响。接下来,我们将看看另一个研究案例。2017 年,Andrew Ng 及其团队发表了一篇开创性的论文,标题为*“CheXNet:利用深度学习在胸部 X 光片中进行放射科医师级的肺炎检测。”* 这篇论文介绍了一种利用深度学习检测胸部 X 光片中肺炎的算法,其性能与专家放射科医师相当。以下图像来自论文。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ChestXNet 原始论文。第一个版本。来源

你注意到有什么问题吗?

在这项研究的第一个版本中,他们训练模型时随机划分数据。由于包含了同一患者的多个扫描图像,这种潜在的数据泄露引发了对 CheXNet 结果可靠性和泛化能力的担忧。作者认识到这个问题,后来发布了新版本,纠正了这个问题。以下图像来自修正版本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ChestXNet 原始论文。最新版本。来源

结论

数据泄露是一个隐蔽的问题,会在开发的各个阶段影响机器学习模型。正如我们在本文中探讨的,它可能导致过拟合、误导性的性能指标,以及最终一个对未见数据无法良好泛化的模型。无论你是在处理表格数据、时间序列还是图像,都需要意识到这一点,以构建成功的模型。以下是本文的一些关键要点:

  • 如果你的模型在进行某些更改后突然表现过于出色,检查是否有数据泄露总是个好主意。

  • 避免在将数据集拆分为训练集和测试集之前对整个数据集进行预处理。相反,使用管道来封装预处理步骤。

  • 在使用交叉验证时,对如过采样或其他任何变换技术要小心。只对每个折中的训练集应用这些技术,以防止数据泄露。

  • 对于时间序列数据,保持观察的时间顺序,并使用诸如基于时间的拆分和时间序列交叉验证等技术。

  • 对于图像数据或具有多个记录的同一受试者的数据集,使用每个受试者的拆分以避免数据泄露。

牢记这些要点,你将能够更好地构建更强大和更准确的机器学习模型。

喜欢这篇文章?通过订阅我的新闻通讯,每周获取数据科学面试问题《数据面试》

你也可以在 LinkedIn上找到我。

产品管理中的五种强大优先级排序技巧

原文:towardsdatascience.com/five-powerful-prioritization-techniques-from-product-management-c44cd1f7e2f3

数据科学家如何从产品经理构建产品、关注以客户为中心的思维以及确保产品成功中获得启发。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Brian Roepke

·发布于 Towards Data Science ·8 分钟阅读·2023 年 4 月 2 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Dayne Topkin 提供,来源于 Unsplash

作为数据科学家、分析师或任何开发产品或解决方案的人,你可以借鉴产品经理用于优先排序需求的工具。在资源和时间有限的情况下,专注于能够为客户带来最大价值的功能和需求是至关重要的。以下是我最喜欢的五种功能和需求优先级排序技巧:

  1. 用户故事映射

  2. 加权优先级矩阵

  3. 重要性与难度矩阵

  4. 购买功能

  5. Kano 模型

每种技巧都有其优点和最适用的情况。尝试所有技巧,了解何时应用每种技巧。让我们逐一了解每种技巧的基本操作以及何时使用它们。

用户故事映射

用户故事映射 是一种通过地图可视化产品功能和需求的强大技巧。它帮助你理解用户的旅程,并根据功能在实现目标中的重要性来优先排序。

执行用户故事映射:

  1. 开始时将产品拆分成较小的用户故事。这些故事应描述用户的目标、需求和痛点。

  2. 组织用户故事,将分组表示为不同的活动及用户完成该活动所需执行的工作流程。

  3. 根据用户故事对用户旅程的重要性进行优先排序(从上到下)。

提示:用户故事映射是一个迭代过程,应根据用户反馈和变化的业务优先级定期更新和修订。此外,涉及你的团队和利益相关者进行映射,以确保对齐和认同。

何时使用:当你开始一个新项目并希望了解用户的工作流程时,这种方法效果很好。这种方法帮助你框定问题,你将肯定会发现一些你之前没有考虑到的领域。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

加权优先级矩阵

加权优先级矩阵是一种基于多个标准对特性进行优先排序的技术。它涉及为每个标准分配权重,并根据每个特性满足每个标准的程度进行评分。然后,将结果得分乘以权重,以计算每个特性的最终得分。

执行加权优先级矩阵的方法:

  1. 确定你想用来评估特性的标准。

  2. 根据每个标准的相对重要性为其分配权重。

  3. 根据每个标准的符合程度对每个特性进行 1 到 n 的评分。你可以使用任何你想要的评分标准,但要保持一致。

  4. 将每个得分乘以其对应的权重,并将结果相加,以计算每个特性的最终得分。作为奖励,你可以平方得分,以给更高的得分更多的权重,例如,平方加权和。

=($B$1*B3*B3)+($C$1*C3*C3)+($D$1*D3*D3)+($E$1*E3*E3)...

在这里下载我的 Excel 示例:加权优先级矩阵

提示:使用加权优先级矩阵时,涉及利益相关者和团队成员的标准选择和权重过程,以确保对齐和认同。此外,要注意你为每个特性打分时尽量客观。你可以将自定义标准(如它如何符合特定业务目标)与传统标准(如风险、收入、紧急程度、努力程度等)混合使用。

何时使用:当你想以更有条理的方式为一长串特性打分时,这种方法非常适用。通过最终得分进行排序,给你一个堆叠的排名列表。因为你与团队合作进行评分系统,它可以减少关于优先级的一些主观性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

重要性与难度矩阵

重要性难度矩阵,也称为价值努力矩阵,是一种根据特性的重要性和难度来优先排序的技术。它涉及将特性映射到一个二维矩阵上,一个轴表示特性的优先级,另一个轴表示实施的难度。

执行重要性难度矩阵的方法:

  1. 确定你想要优先排序的特性。

  2. 将每个特性在水平轴上从最不重要到最重要排序。确保没有两个特性在同一列中。

  3. 接下来,将每个特性在垂直轴上按难度从低到高排序。确保没有两个特性在同一行。

提示:在使用重要性困难矩阵时,涉及你的团队和利益相关者进行评分,以确保对优先级的对齐和认同。此外,排序过程是最重要的部分,你正在对优先级达成一致。此外,每个特性必须在不同的列或行中,确保没有两个特性被视为同等重要或困难。

何时使用:当你有大约十个项目需要排序时,这是一个很好的协作练习。这个减少的集合可以是你已筛选并希望获得绝对优先级的项目列表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

购买特性

购买特性是一种技术,涉及给客户一定数量的资金购买他们希望在产品中看到的特性。客户可以将预算分配到他们认为最重要的特性上。这种技术可以帮助产品经理了解客户最需求的特性,并据此进行优先级排序。

要执行“购买特性”:

  1. 确定你想要优先考虑的特性。

  2. 为每个特性分配一个货币价值,并给客户一个“资金”预算来支出。

  3. 你可以使用实物或数字代币来代表资金,让客户实际“购买”他们想要的特性。一旦所有客户花完预算,测试结果以确定哪些特性最受欢迎。

提示:为了确保“购买特性”的成功,请为流程设定明确的规则和指导方针。对预算及每个特性的费用保持透明。确保涉及到代表性客户群体以获得广泛的视角。此外,考虑向不同客户群体提供不同的预算,因为某些客户可能对某些特性更看重。

何时使用:当你直接与客户或利益相关者合作时,他们坚持认为一切都是高优先级的。“购买特性”技术迫使客户决定他们真正想要什么。它在一个可以设置非常困难的特性值高于一个人分配金额的群体中效果很好,这会强迫一个协作讨论,其中多个人需要将资源汇集在一起。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

Kano 模型

Kano 模型是一种识别哪些特性将为客户提供最大价值的技术。它将特性分类为不同类型:

  • 必备特性:这些是客户期望的基本要求。这些特性如你车上的刹车,你不会买没有这些特性的车。

  • 性能特性:这些为客户提供递增的价值。这些特性可以是更好的燃油效率或更强大的引擎。

  • 令人惊喜的功能:这些是能够给客户带来惊喜的意外功能。这些功能包括加热方向盘或天窗。

执行 Kano 模型:

  1. 确定客户需求:使用 Kano 模型的第一步是识别与您的产品或服务相关的客户需求,这可以通过调查、焦点小组或客户访谈来完成。

  2. 分类客户需求:一旦识别出客户需求,你需要将它们分类为三类:基本需求、表现需求和令人惊喜的功能。

  3. 确定满意度水平:接下来,你需要确定客户对每个识别出的需求的满意度水平。可以使用李克特量表(例如,1-5)来测量客户满意度。

  4. 绘制数据:一旦你有了每个需求的满意度水平,将它们绘制在 Kano 模型图上。在横轴上绘制需求的表现或实施水平,在纵轴上绘制客户的满意度水平。

  5. 分析结果:根据绘制的数据,你可以识别出每个类别(基本需求、表现需求和令人惊喜的功能)中的需求。此外,你还可以识别出当前未满足并需要改进的需求。

  6. 制定行动计划:根据分析,制定一个行动计划,以满足客户的需求,包括改善基本需求、优化表现需求或投资于令人惊喜的功能。

  7. 重复过程:Kano 模型不是一次性的练习。为了确保满足客户需求,定期重复这一过程非常重要,以识别客户需求和满意度水平的变化。

提示:请记住,客户需求和期望可能会随时间变化,因此定期重新评估功能的分类非常重要。此外,要小心不要过于关注令人惊喜的功能而忽略基本需求,因为关注基本需求可以带来客户满意度。此外,在进行调查或访谈时,使用足够大的样本量以确保数据能够代表您的客户群体。

何时使用:这是优先排序的更全面技术之一,掌握它可能需要一段时间。我建议阅读Folding Burritos 上的 Kano 模型完整指南

结论

优先级排序是任何构建产品的人都需要掌握的关键技能。优先级排序要求深入了解客户的需求、业务目标和资源。我们讨论的五种技术——用户故事映射、加权优先级矩阵、重要性与难度矩阵、购买功能和卡诺模型——提供了不同的优先级排序方法。尽管如此,它们的终极目标都是最大化客户和业务的价值。记得在优先级排序过程中涉及你的团队和利益相关者,定期重新评估你的优先级,并专注于为客户提供价值。有效运用这些技术,你可以创造出让客户满意并推动业务成功的产品。

如果你喜欢阅读这样的故事,并希望支持我作为作家,考虑注册成为 Medium 会员。每月 5 美元,享有对数千篇文章的无限访问。如果你使用 我的链接注册,我将获得一小笔佣金,但不会额外产生费用。

五种实际应用 LSTM 模型于时间序列的案例,附代码

原文:towardsdatascience.com/five-practical-applications-of-the-lstm-model-for-time-series-with-code-a7aac0aa85c0?source=collection_archive---------0-----------------------#2023-09-22

如何在多个不同的时间序列背景下实现高级神经网络模型

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Michael Keith

·

关注 发表在 Towards Data Science ·11 min read·Sep 22, 2023

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源于 Andrew SvkUnsplash

当我在 2022 年 1 月写了探索 LSTM 神经网络模型在时间序列中的应用时,我的目标是展示如何使用我开发的时间序列库scalecast在 Python 中轻松实现这个先进的神经网络。我没想到它会被观看超过数万次,并在我发布后超过一年内在 Google 搜索“lstm forecasting python”时排名第一(今天检查时,它仍然是第二)。

我并没有尝试引起对那篇文章的过多关注,因为我从未认为,也仍然认为,它不是很好。它从未打算成为实现 LSTM 模型最佳方法的指南,而只是简单探讨其在时间序列预测中的实用性。我试图回答诸如:当你使用默认参数运行模型时会发生什么,当你以这种或那种方式调整参数时会发生什么,它在某些数据集上被其他模型击败的难易程度等问题。然而,根据博客文章、Kaggle 笔记本,甚至我不断看到的Udemy 课程,这篇文章的代码被逐字复制,很明显许多人把它当作前者的价值,而不是后者。我现在明白我没有清晰地表达我的意图。

今天,为了扩展那篇文章,我想展示如何应用 LSTM 神经网络模型,或者至少是我如何应用它,以充分发挥其在时间序列预测问题中的价值。自从我写了第一篇文章以来,我们已经能够为 scalecast 库添加许多新的创新功能,使得使用 LSTM 模型更加无缝,我将在这里探讨一些我最喜欢的功能。我认为 LSTM 有五种应用会在这个库中表现得非常出色:单变量预测、多变量预测、概率预测、动态概率预测和迁移学习

在开始之前,请确保在终端或命令行中运行:

pip install --upgrade scalecast

为本文开发的完整笔记本位于这里。

最后一点:在每个示例中,我可能会将“RNN”和“LSTM”互换使用。或者,RNN 可能会显示在 LSTM 预测的某个图表上。长短期记忆(LSTM)神经网络是一种递归神经网络(RNN),具有额外的记忆相关参数。在 scalecast 中,rnn模型类可以用来拟合从tensorflow移植过来的简单 RNN 和 LSTM 单元。

1. 单变量预测

使用 LSTM 模型最常见且最明显的方式是处理简单的单变量预测问题。尽管该模型拟合了许多参数,使其足够复杂以有效学习任何给定时间序列中的趋势、季节性和短期动态,但我发现它在处理平稳数据(即不表现出趋势或季节性的数据显示)时效果更好。因此,使用可在Kaggle上获取的航空乘客数据集,我们可以仅通过去趋势和去季节性处理数据,使用相当简单的超参数来创建准确可靠的预测:

transformer = Transformer(
    transformers = [
        ('DetrendTransform',{'poly_order':2}),
        'DeseasonTransform',
    ],
)

我们还要确保在完成后将结果恢复到其原始水平:

reverter = Reverter(
    reverters = [
        'DeseasonRevert',
        'DetrendRevert',
    ],
    base_transformer = transformer,
)

现在,我们可以指定网络参数。在这个示例中,我们将使用 18 个滞后、一个层、一个 tanh 激活函数和 200 个训练周期。随意探索你自己的、更好的参数!

def forecaster(f):
    f.set_estimator('rnn')
    f.manual_forecast(
        lags = 18,
        layers_struct = [
            ('LSTM',{'units':36,'activation':'tanh'}),
        ],
        epochs=200,
        call_me = 'lstm',
    )

将所有内容结合成一个管道,运行模型,并从视觉上查看结果:

pipeline = Pipeline(
    steps = [
        ('Transform',transformer),
        ('Forecast',forecaster),
        ('Revert',reverter),
    ]
)

f = pipeline.fit_predict(f)

f.plot()
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

足够好,比我在另一篇文章中展示的任何内容都要好得多。要扩展这个应用,你可以尝试使用不同的滞后阶数,将季节性以傅里叶项的形式添加到模型中,寻找更好的序列转换,并通过交叉验证来调整模型的超参数。后续部分将演示其中的一些方法。

2. 多变量预测

假设我们有两个序列,我们预计它们会一起变化。我们可以创建一个 LSTM 模型,在进行预测时考虑这两个序列,希望提高模型的整体准确性。这就是所谓的多变量预测。

在这个示例中,我将使用可在Kaggle上获取的鳄梨数据集。它测量了不同美国地区的鳄梨价格和销售数量。根据经济理论,我们知道价格和需求是密切相关的,因此使用价格作为领先指标,我们可能会比仅使用历史需求更准确地预测鳄梨的销售量。

我们首先要做的是转换每个序列。我们可以通过运行以下代码来搜索一组“最佳”转换(即在样本外得分的转换):

data = pd.read_csv('avocado.csv')

# demand
vol = data.groupby('Date')['Total Volume'].sum()
# price
price = data.groupby('Date')['AveragePrice'].sum()

fvol = Forecaster(
    y = vol,
    current_dates = vol.index,
    test_length = 13,
    validation_length = 13,
    future_dates = 13,
    metrics = ['rmse','r2'],
)

transformer, reverter = find_optimal_transformation(
    fvol,
    set_aside_test_set=True, # prevents leakage so we can benchmark the resulting models fairly
    return_train_only = True, # prevents leakage so we can benchmark the resulting models fairly
    verbose=True,
    detrend_kwargs=[
        {'loess':True},
        {'poly_order':1},
        {'ln_trend':True},
    ],
    m = 52, # what makes one seasonal cycle?
    test_length = 4,
)

从这个过程中推荐的变换是季节调整,假设 52 个周期构成一个季节,以及一个鲁棒缩放(对异常值鲁棒的缩放)。然后,我们可以在系列上拟合该变换,并调用单变量 LSTM 模型,以便与多变量模型进行基准对比。这一次,我们将使用超参数调优过程,生成可能的激活函数、层大小和丢弃值的网格。

rnn_grid = gen_rnn_grid(
    layer_tries = 10,
    min_layer_size = 3,
    max_layer_size = 5,
    units_pool = [100],
    epochs = [25,50],
    dropout_pool = [0,0.05],
    callbacks=EarlyStopping(
      monitor='val_loss',
      patience=3,
    ),
    random_seed = 20,
) # creates a grid of hyperparameter values to tune the LSTM model

这个函数提供了一种将可管理的网格输入到我们的对象中的良好方式,同时也有足够的随机性,以便有一个好的参数候选集。现在我们拟合单变量模型:

fvol.add_ar_terms(13) # the model will use 13 series lags
fvol.set_estimator('rnn')
fvol.ingest_grid(rnn_grid)
fvol.tune() # uses a 13-period validation set
fvol.auto_forecast(call_me='lstm_univariate')

为了将其扩展到多变量背景下,我们可以用与其他系列相同的变换集来转换价格时间序列。然后,将 13 个价格滞后值输入Forecaster对象中,并拟合一个新的 LSTM 模型:

fprice = Forecaster(
    y = price,
    current_dates = price.index,
    future_dates = 13,
)

fprice = transformer.fit_transform(fprice)

fvol.add_series(fprice.y,called='price')
fvol.add_lagged_terms('price',lags=13,drop=True)
fvol.ingest_grid(rnn_grid)
fvol.tune()
fvol.auto_forecast(call_me='lstm_multivariate')

我们还可以基准化一个简单模型,并在原始系列水平绘制结果,以及样本外测试集:

# naive forecast for benchmarking
fvol.set_estimator('naive')
fvol.manual_forecast()

fvol = reverter.fit_transform(fvol)

fvol.plot_test_set(order_by='TestSetRMSE')
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者提供的图像

从三种模型的视觉聚类来看,这一系列数据的准确性主要得益于所应用的变换——这也是为什么简单模型与 LSTM 模型的表现如此接近的原因。不过,LSTM 模型确实有所改进,多变量模型的得分和 r 平方为 38.37%,单变量模型为 26.35%,而基准为-6.46%。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者提供的图像

可能阻碍 LSTM 模型在这一系列数据上表现更好的一个因素是数据集的长度。仅有 169 个观察值,可能不足以让模型充分学习模式。然而,相较于某些简单模型的改进,任何提升都可以被视为成功。

3. 概率预测

概率预测是指模型不仅能够进行点预测,还能够提供预测偏差的估计。概率预测类似于使用置信区间进行预测,这一概念已经存在很长时间了。产生概率预测的一个新兴方法是通过将一致性置信区间应用于模型,利用校准集来确定实际未来点的可能分布。这种方法的优点在于可以适用于任何机器学习模型,无论该模型对输入或残差的分布做出什么假设。它还提供了对任何机器学习从业者都非常有用的覆盖保证。我们可以将一致性置信区间应用于 LSTM 模型以产生概率预测。

在这个例子中,我们将使用FRED上提供的月度住房开工数据集,这是一个经济时间序列的开放数据库。我将使用 1959 年 1 月到 2022 年 12 月的数据(768 个观察值)。首先,我们将再次搜索最佳的变换集合,但这次使用一个具有 10 个周期的 LSTM 模型来评分每次变换尝试:

transformer, reverter = find_optimal_transformation(
    f,
    estimator = 'lstm',
    epochs = 10,
    set_aside_test_set=True, # prevents leakage so we can benchmark the resulting models fairly
    return_train_only = True, # prevents leakage so we can benchmark the resulting models fairly
    verbose=True,
    m = 52, # what makes one seasonal cycle?
    test_length = 24,
    num_test_sets = 3,
    space_between_sets = 12,
    detrend_kwargs=[
        {'loess':True},
        {'poly_order':1},
        {'ln_trend':True},
    ],
)

我们将再次随机生成一个超参数网格,但这次我们可以将其搜索空间设置得非常大,然后在模型拟合后手动将其限制为 10 次尝试,以便在合理的时间内对参数进行交叉验证:

rnn_grid = gen_rnn_grid(
    layer_tries = 100,
    min_layer_size = 1,
    max_layer_size = 5,
    units_pool = [100],
    epochs = [100],
    dropout_pool = [0,0.05],
    validation_split=.2,
    callbacks=EarlyStopping(
      monitor='val_loss',
      patience=3,
    ),
    random_seed = 20,
) # make a really big grid and limit it manually

现在我们可以构建和拟合管道:

def forecaster(f,grid):
    f.auto_Xvar_select(
        try_trend=False,
        try_seasonalities=False,
        max_ar=100
    )
    f.set_estimator('rnn')
    f.ingest_grid(grid)
    f.limit_grid_size(10) # randomly reduce the big grid to 10
    f.cross_validate(k=3,test_length=24) # three-fold cross-validation
    f.auto_forecast()

pipeline = Pipeline(
    steps = [
        ('Transform',transformer),
        ('Forecast',forecaster),
        ('Revert',reverter),
    ]
)

f = pipeline.fit_predict(f,grid=rnn_grid)

因为我们在Forecaster对象中预留了足够大小的测试集,所以结果自动为每个点估计提供了 90%的概率分布:

f.plot(ci=True)
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

4. 动态概率预测

之前的示例提供了一个静态的概率预测,其中预测的每个上界和下界距离点估计的距离与其他点的上界和下界相等。当预测未来时,直观上看,预测越远,误差的扩散范围就越宽——这一细微差别在静态区间中没有体现。通过使用回测,有一种方法可以实现更动态的概率预测。

回测是一个迭代地重新拟合模型、在不同预测范围内进行预测并测试其性能的过程。让我们以最后一个示例中指定的管道为例,对其进行 10 次回测。我们需要至少 10 次回测迭代来构建 90%置信区间:

backtest_results = backtest_for_resid_matrix(
    f,
    pipeline=pipeline,
    alpha = .1,
    jump_back = 12,
    params = f.best_params,
)

backtest_resid_matrix = get_backtest_resid_matrix(backtest_results)

我们可以通过可视化分析每次迭代中残差的绝对值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

这个特定示例有趣之处在于,最大的误差通常不在预测的最后几个步骤上,而是在步骤 14-17 之间。这种情况可能发生在具有奇特季节性模式的序列中。异常值的存在也可能影响这种模式。不管怎样,我们可以利用这些结果现在用动态区间替换静态置信区间,这些动态区间在每一步都是符合的。

overwrite_forecast_intervals(
    f,
    backtest_resid_matrix=backtest_resid_matrix,
    alpha=.1, # 90% intervals
)
f.plot(ci=True)
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

5. 转移学习

转移学习在我们希望在模型适配的上下文之外使用模型时很有用。我将演示其实用性的两个具体情景:在给定时间序列中有新数据可用时进行预测,以及对具有类似趋势和季节性的相关时间序列进行预测。

情景 1:来自同一序列的新数据

我们可以使用与之前两个示例相同的住房数据集,但假设已经过去了一段时间,我们现在有数据可用到 2023 年 6 月。

df = pdr.get_data_fred(
    'CANWSCNDW01STSAM',
    start = '2010-01-01',
    end = '2023-06-30',
)

f_new = Forecaster(
    y = df.iloc[:,0],
    current_dates = df.index,
    future_dates = 24, # 2-year forecast horizon
)

我们将重新制作管道,使用相同的转换,但这次使用转移预测,而不是适配模型的正常尺度预测过程:

def transfer_forecast(f_new,transfer_from):
    f_new = infer_apply_Xvar_selection(infer_from=transfer_from,apply_to=f_new)
    f_new.transfer_predict(transfer_from=transfer_from,model='rnn',model_type='tf')

pipeline_can = Pipeline(
    steps = [
        ('Transform',transformer),
        ('Transfer Forecast',transfer_forecast),
        ('Revert',reverter),
    ]
)

f_new = pipeline_can.fit_predict(f_new,transfer_from=f)

尽管相关函数的名称仍然是fit_predict(),但实际上在管道中没有拟合,只有预测。这大大减少了我们需要重新拟合和重新优化模型的时间。然后我们查看结果:

f_new.plot()
plt.show('Housing Starts Forecast with Actuals Through June, 2023')
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

情景 2:具有相似特征的新时间序列

对于第二种情景,我们可以使用一个假设的情况:希望利用在美国住房动态上训练的模型来预测加拿大的住房开工情况。免责声明:我不知道这是否真的一个好主意——这只是我想到的一个场景,用来演示如何完成这一任务。但我认为这可能会有用,而且相关代码可以转移到其他情况(例如,对于那些你拥有的短时间序列,其动态类似于你已经适配了表现良好的模型的较长序列)。在这种情况下,代码实际上与情景 1的代码完全相同;唯一的区别是我们加载到对象中的数据:

df = pdr.get_data_fred(
    'CANWSCNDW01STSAM',
    start = '2010-01-01',
    end = '2023-06-30',
)

f_new = Forecaster(
    y = df.iloc[:,0],
    current_dates = df.index,
    future_dates = 24, # 2-year forecast horizon
)

def transfer_forecast(f_new,transfer_from):
    f_new = infer_apply_Xvar_selection(infer_from=transfer_from,apply_to=f_new)
    f_new.transfer_predict(transfer_from=transfer_from,model='rnn',model_type='tf')

pipeline_can = Pipeline(
    steps = [
        ('Transform',transformer),
        ('Transfer Forecast',transfer_forecast),
        ('Revert',reverter),
    ]
)

f_new = pipeline_can.fit_predict(f_new,transfer_from=f)

f_new.plot()
plt.show('Candian Housing Starts Forecast')
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

我认为该预测看起来足够可信,作为 LSTM 转移学习的一个有趣应用。

结论

对于许多预测用例,LSTM 模型可能是一个有趣的解决方案。在这篇文章中,我演示了如何使用 Python 代码将 LSTM 模型应用于五种不同的目的。如果你觉得这很有用,请在 GitHub 上给scalecast 点个星,并务必在 Medium 上关注我,以便及时了解包的最新动态。如果你有反馈、建设性的批评或对这段代码有任何疑问,随时可以通过电子邮件联系我:mikekeith52@gmail.com。

五个协作数据科学的软件工程原则

原文:towardsdatascience.com/five-software-engineering-principles-for-collaborative-data-science-ab26667a311?source=collection_archive---------9-----------------------#2023-01-13

可重复的数据科学项目需要项目组织和干净的代码

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Jo Stichbury

·

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

传统的软件工程师在代码中制定规则。相比之下,数据科学家依赖于学习算法来分析数据中的模式。然而,分析项目仍然需要依赖传统代码,作为数据科学家,你可以从最初由软件工程领域首创的最佳实践中获益。

用一个比喻来说,假设你是一位厨师,正在准备一顿丰盛的晚餐。在厨房里,你有不同的食材和工具,你的工作是使用这些工具,并将食材以正确的方式组合在一起,做出美味的菜肴。你和你的顾客都希望你的菜肴既美味又不被烤焦或生煮。一间杂乱无章的厨房让其他人很难做出你的菜肴,因此一位优秀的厨师会花时间保持厨房整洁,标记食材,并在一套食谱中记录下菜肴的制作过程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Louis Hansel 的照片,来自 Unsplash

同样,作为数据科学家,你希望我们的输出和洞察是正确且可重复的。在你的“代码厨房”中遵循最佳实践,有助于你创建有序的代码,这样你和其他人在未来可以继续进行分析项目,理解、扩展和重用它。

在这篇文章中,我们将讨论一些方法,以使你的“胶水代码”既可靠又高效且易于理解。当你开始在原型上编写代码时,可能不会优先考虑可维护性和一致性,但采用一种已经被证明有效的文化和工作方式,可以让你的原型更快地准备好投入生产。

“精心设计的数据科学代码可以帮助你解锁有价值的洞察。”

1. 使用标准且逻辑的项目结构

虽然数据科学旨在生成一系列洞察,如报告和可视化,但考虑生成这些洞察的程序代码的质量也是至关重要的。虽然数据实验可能会有偶然的结果,但你和你的潜在同事需要能够扩展实验并在未来重新运行它。

最好从相同的、一致的、逻辑的项目结构开始每个实验。任何查看项目的人都可以理解布局,而不需要 extensive documentation。组织良好的代码通常具有自我文档化的特点,因为它提供了上下文。

对每个项目使用相同的结构有助于可重复的协作,这意味着你可以对分析得出的结论充满信心。你是否曾尝试重现几个月前做的事情?你可能当时掌握了所有细节,但如果项目被搞得一团糟,返回时几乎要从头开始。

一致且可靠的项目使你更容易回顾和与他人分享,以便你的团队成员可以轻松维护和修改你的项目。

没有唯一的对或错的方法,但你应该采用一种语义文件夹结构,其中位置编码了含义(例如,配置、数据、文档、笔记本和源代码的文件夹)。这种方法使项目导航变得容易,因为对象的位置描述了其用途。

如果你在寻找灵感,可以查看 DrivenData 关于 CookieCutter Data Science 的页面,他们描述其为“一个逻辑性强、相对标准化但灵活的数据科学工作和分享的项目结构”。并且看看开源项目 Kedro,它基于 CookieCutter Data Science 的学习,提供可修改的项目启动模板以自定义你的模板。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源:Max KomthongvijitUnsplash

2. 通过依赖管理使你的环境可重复

大多数数据科学 Python 代码会导入第三方包,这些包提供可重用的功能。这些包有不同的版本,通常对其他 Python 包的特定版本有依赖关系。

依赖管理记录了你的项目的确切工作环境,因此可以通过安装等效的包集来在不同的干净环境中轻松重现设置。

一种选择是将每个包依赖和子依赖的列表写入文档中。推荐的方法是使用标准化、可重复、广泛接受的格式来列出这些信息,比如 pip install 的输入。

对于你的项目直接依赖的每个包,列出它及其需要的版本以进行“固定”。包可能会频繁更新;固定可以保护你免受更改引入的错误或不兼容变化的影响。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来源:Duane MendesUnsplash

虚拟环境

如果你在使用 Python,始终为你的项目使用虚拟环境,以保护环境免受全局环境潜在更改的影响。例如,如果你依赖于 Pandas 项目中 2021 年引入的一个功能,但另一个项目需要较旧的版本,那么如果你在一个全局空间中工作,就会出现冲突。

为每个项目保持一个独立的干净环境,例如使用 condavenv,可以确保更好的项目可重复性,因为你可以避免版本冲突。

了解更多关于为什么你需要虚拟环境

在 YouTube 上了解 Python 虚拟环境工具和工作流:youtu.be/YKfAwIItO7M

3. 通过使代码可读来提高其可重用性

《代码整洁之道:敏捷软件工艺手册》是一本 2008 年的软件工程书籍,提出了无论使用什么编程语言或其目的,都应遵循的最佳实践。它提出了从头开始编写良好代码和改进糟糕代码的几个原则,并描述了“代码异味”,这些异味表明你的代码“出了问题”。

除了阅读书籍,你还可以找到许多视频培训课程和书籍总结,根据你需要的详细程度。我的意图不是在这里复述所有内容,而是考虑书中描述的一个方面:代码可读性。

“代码被阅读的频率远高于编写的频率。”

虽然似乎没有单一来源的引述,但它常被归于 Python 编程语言的创建者 Guido van Rossum 和里程碑PEP8 文档的贡献者,该文档提供了编写可读代码的指导

你可以通过遵循常见标准和惯例,并要求你的团队进行代码审查来提高代码的可读性。你可能会在开始时集中精力于代码的功能,但如果你在编写代码时使其可读,你会发现后续工作更简单。清晰有助于调试,如果其他人检查过并确认理解你的方法以及遵循一些基本规则,你会发现维护起来更容易。

在查看你的代码(或别人的代码)时,有几个提示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可读代码的要素:Jo Stichbury(2023)公有领域

  • 初学者常常使用缩写或短名称来命名函数和变量。如果你没有编写这些代码,很难理解它们;即使你编写了,也会发现几个月后很难理解。创建有意义的名称

  • 最好的代码是自我文档化的,意味着理解它所需的注释很少,但注释有助于在函数级别记录非平凡的代码。只要不要写出重复代码的大块文字即可。

  • 通过使用空白来使代码可读。如果你使用 Python,你会发现这很简单,因为 Python 给空白赋予了语法意义。

  • 编写只做一件事的小函数,具有单一返回路径和有限数量的参数。

  • 不要使用硬编码的值;相反,使用精确命名的常量,并将它们全部放入一个配置文件中,以便你可以轻松找到和更新它们。像 OmegaConfpython-anyconfigPyYAML 这样的配置管理工具旨在帮助实现这一点。

不要忘记文档

文档还可以帮助代码的可读性,并且在详细程度上有所不同:

  • 基本内联注释

  • 来自 docstrings 的 API 文档,解释如何使用/重用函数

  • Markdown 文件,如 GitHub 仓库根目录中的 README 页面,解释项目设置或特定使用细节。

保持你的文档最新,否则它可能会误导人,比没有文档更糟,并投入一些时间学习如何构建你的文档以发布它们,使用像 Jekyll 或 Sphinx 这样的工具。

4. 将笔记本代码重构为管道

到目前为止,这些建议已经足够通用,以至于初级软件工程师和数据科学家都能理解。这一点适用于处理数据摄取、转换、模型训练、评分和评估的顺序。

使用 Python 函数和包来形成管道可以对任务执行的顺序进行编码。有几个开源解决方案可以帮助构建这些类型的管道,例如 GNU Make,这是一个通用的遗留工具,仍然满足许多现代数据科学的需求,以及用于 Python 的 Snakemake。其他受欢迎的管道工具包括 KedroLuigiMetaflowAirflowPrefectPloomber。选择工具时,你应该考虑学习曲线以及是否需要额外的功能,如能够调度管道运行。

管道的好处

可重复性:任何人都可以用很少的努力从原始数据中再现结果

正确性:结果是可以测试的

可读性:新的团队成员可以理解并掌握管道

扩展性:你可以将一个小的管道扩展为处理多个数据源、使用不同的模型和生成报告。

可维护性:你可以编辑和重新测试。正如我们之前描述的那样,Jupyter 笔记本非常适合快速原型,但它们就像你家门口的桌子或装满杂物的抽屉。无论你的意图多么良好,这里总会堆积杂乱的东西,如硬编码常量、打印语句调试和未使用的代码。笔记本中的代码越多,你就越难确定你编写的代码是否按预期工作。

测试,测试

使用管道可以将功能放入 Python 模块中,这样你可以测试、更新并再次测试,避免了“僵尸代码”对解释的干扰。pytest 框架 可以帮助你完成这些任务。

编写测试!如果你在“完成”的定义中包括编写测试(例如单元测试、集成测试和数据验证测试),你就不能跳过它们,并且在估算工作量时会将其计算在内。而且,良好的测试也可以作为文档,因为阅读测试可以帮助理解代码的功能。

在大多数数据科学项目中,大部分代码用于数据转换,而只有一小部分代码库是实际的机器学习。数据转换代码的管道可以进行测试(按定义,它们对相同输入应返回相同输出)。即使是机器学习代码也可以进行测试,以确认其是否按预期工作。你可以编写功能测试,以检查模型的指标(例如准确度、精确度等)是否超过预期阈值。

“尽早将代码从笔记本中移到 Python 模块和包中,以形成管道,以管理复杂性。”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Josué AS 提供,来源于 Unsplash

5. 投资一些时间掌握版本控制

版本控制系统(VCS)如 GitMercurialSubversion 允许你存储代码的检查点版本,以便你可以修改代码但在以后回滚到先前的版本。这就像拥有一系列备份,更重要的是,你可以与其他开发人员共享这些备份。

投资一些时间学习版本控制的原则,以最大化其带来的价值,这样你可以处理更复杂的场景,例如棘手的合并。还有一些优秀的实践材料可供学习,例如 Git 快速入门指南学习 Git 分支教程

在 YouTube 上 15 分钟学会 Git: youtu.be/USjZcfj8yxE

一些数据科学家学习了 commit 的基础知识,但这里有一些最佳实践需要考虑:

  • 经常提交:如果你在大幅修改代码而没有提交更改,你可能会冒着失去已经花费时间的风险,因为不小心添加了一个破坏代码的更改,之后可能无法回滚。此外,喝咖啡时要小心放在笔记本电脑旁边!如果其他人也在进行代码修改,代码库可能会发生变化,而当你进行提交时,你可能会面临冲突和合并地狱。

  • 只提交你需要的:如果文件是为了你的个人本地配置、秘密,例如数据库登录凭证或构建结果生成的中间文件,则不应存储所有文件。学习如何使用.gitignore

  • 不要在版本控制中存储原始数据。原始数据不会改变,所以你不需要对其进行版本控制。对于可以从原始数据和你的代码生成的中间数据文件也是如此。如果需要跟踪转化后的数据,可以使用不同的数据/工件/工作流版本控制工具,如 DVC(数据版本控制)或 Pachyderm,这些工具可以扩展你的 Git 代码版本控制。

使用版本控制在自己的项目和团队中有诸多好处,你可以大胆尝试任何代码中的风险,因为你可以迅速恢复到一个已知的、可用的状态。将稳固的版本控制系统流程与测试结合起来,提供了一种强大的工作方式。当你的代码产生正确结果时,保存它;然后在下一次更改代码时,重新运行测试。如果测试通过,说明你可能没有破坏任何东西。如果测试不通过,你可以重新工作或恢复。

摘要

自从哈佛商业评论 反映出对数据科学家的日益增长需求 已经过去了十多年:那些能够结合编程、分析和实验技能的人。虽然进入数据科学的职业道路仍然没有明确的定义,但现在有很多学习的途径,包括:

通常,这个角色吸引那些对数学有自信、乐于尝试复杂且凌乱的数据集并且能够编程的人。

数据科学现在可能已经确立了自己的角色,但软件开发已经有了几十年的成熟和经验积累。数据科学家可以学习的一些最有价值的技术是那些几代软件工程师建立的技术,例如结合版本控制、测试、可读的干净代码和良好的文件夹结构。这些可以使生产级项目成功与在原型阶段停滞之间产生差异。

如果你是寻求灵感的数据科学家,可以借鉴这些工程最佳实践以实现长期分析成功。

五件事 GenAI 能做和不能做的事情

原文:towardsdatascience.com/five-things-genai-can-and-cant-do-d8117aad82f4

为商业领袖提供的关于生成型 AI 能做或不能做的介绍性指南

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 David Hundley

·发表于 数据科学的前沿 ·11 分钟阅读·2023 年 10 月 7 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

封面照片由作者创作

难以置信的是,自 ChatGPT 发布以来还不到一年,我们已经看到生成型 AI(GenAI)席卷了整个世界。从大型语言模型(LLM)到稳定扩散模型用于图像生成,这项新技术所能做的确实令人惊叹。一位朋友告诉我,这是 AI 第一次让他们感到触手可及,就像我们通过科幻小说梦想到的东西现在已经变成现实。

自然地,这让商业领袖们开始思考 GenAI 能或不能做什么来转变他们的业务流程。确实,你可以用 GenAI 做很多酷的事情,但也有一些流传的误解,商业领袖们应该小心。本文的重点是与您分享 GenAI 能够做的一些核心事情,同时也提醒对其不能做的事情保持适当的期望。

能做的 #1:GenAI 可以总结大量信息。

也许我听到的所有行业中最经典的用例之一是,特别是利用大型语言模型(LLM)将大量信息压缩成更易于消化的内容。例如,你可以将会议的转录对话交给 GenAI,总结成几个关键要点。此外,你可以将一份大型法律文件交给 LLM,让它提取出最相关的信息。当然,你应该始终小心核实 LLM 的输出是否正确,但这在许多不同的业务环境中可以节省大量时间。我高度预期这将在更多行业中持续获得关注。

不能做的 #1:GenAI 永远无法对任何事情有确切的确定性。

也许关于 LLM 的最大误解之一是它们能够思考。实际上,LLM 只是预测词汇的机器,尽管这些模型的精确度非常高,以至于看起来它们似乎在模拟真正的意识。由于 LLM 是基于词汇之间的概率进行操作的,它永远不能真正确定最终输出。然而,具有讽刺意味的是,它总是会产生非常自信的输出。我们将这些自信但错误的陈述称为虚构

考虑以下句子:“I like to drink ______ in the morning。”如果你作为一个人来回答这个问题,可能会挠头。我们可以相当确定空白处是某种液体,但具体是什么液体呢?咖啡?茶?水?就像人类一样,LLM 也不能确定答案;然而,不同于人类,LLM 不会告诉你“我不知道”。相反,它会自信地给出一个答案,尽管你和我知道 LLM 不能真正确定答案。让我们实际看看 ChatGPT 如何尝试填补这个空白。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者拍摄的 iPad 上 ChatGPT 的截图

如你所见,ChatGPT 自信地将空白填为“coffee”,但它没有表明也许填错了。我们再次将这种过度自信的“猜测”称为虚构,但我们可以通过下一个要点来减少这些虚构……

#2:GenAI 可以通过增强的上下文提供更有根据的答案。

在前一点中,我们提到 LLM 对以下句子中的空白填充没有特别确定性:“I like to drink ______ in the morning。”然而,我们可以通过提供额外的上下文来增强 LLM 的知识。在 GenAI 社区,我们将此称为检索增强生成(RAG)。如果我们要求 LLM 在没有任何额外上下文的情况下填充上述空白,它肯定会给出一个答案,但那个答案可能是虚构的。现在,假设我们给 LLM 一些额外的上下文。让我们修改输入给 LLM 的内容如下:

我的名字是 David Hundley。我喜欢每天早晨喝一杯 Starbucks 的冷萃咖啡。使用这些信息,请填补以下句子中的空白:“I like to drink ______ in the morning。”

现在,LLM 已经获得了一些非常具体的上下文,它可以充分填补空白。在 ChatGPT 中测试时,你会看到我们收到的是精确的正确答案。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由作者拍摄的 iPad 上 ChatGPT 的截图

当然,这仍然不会使 LLM 的输出完全可靠。请记住,LLM 在评估词汇之间的概率。在 RAG 过程中,我们只是通过更正确的答案增强了输出的概率。这并不会将确定性的概率提升到 100%,但确实可以提供很大帮助!

无法解决 #2:GenAI 不能自己解决新问题。

尽管 RAG 过程可以在很大程度上帮助 GenAI 提供更精确的回答,但 RAG 过程并不是自动的。换句话说,像 LLMs 这样的 GenAI 技术没有自我学习的能力。这些 GenAI 模型是在某一时间点提供的信息上进行训练的,几乎就像它们的知识被冻结在那个固定日期。当我们向 LLM 提供额外的上下文时,这些上下文可以帮助生成更精确的输出,但这个 RAG 上下文实际上并不会改变基础模型本身

我提到这一点的原因是,尽管 RAG 过程可能很有用,但你不能让 GenAI 自行去寻找这些信息。即使像 Bing Chat 这样的案例中,LLM 似乎在搜索互联网,但实际上并不是模型在爬取互联网。而是信息被带到 LLM,LLM 在理解这些带来的信息。 我们还没有达到那种像天网一样的超级智能水平,能够让这些 AI 模型自己解决问题。🤖

可以解决 #3:GenAI 可以是一个很棒的编码助手。

请特别注意这一点的措辞。我在这个点上对“能”这个词非常讲究。人们已经发现了利用 LLM 进行编码的方式,这并不是什么秘密。明确来说,LLM 在帮助编写代码方面可以非常出色。无论是自动填充常见任务还是帮助调试错误,GenAI 在编码时可以是一个非常有用的工具。

问题在于…… LLM 在这个任务上并不完美。除了受限于在某一时间点的训练信息外,根据我的个人经验,LLM 在被要求编写非常细致的代码时,往往会出现幻觉现象。请记住,LLM 仅能根据你提供的上下文来工作。 如果你有一个源于你非常特定系统的错误,LLM 无法了解你系统的细节,因此最终会产生幻觉性的答案。这并不是说 GenAI 无法在编写代码时提供帮助,但我认为它更像是一个助手。

无法解决 #3:GenAI 无法确定另一段内容是否由 GenAI 创建。

这是一个研究人员来回摇摆的点。实际上,OpenAI 曾经发布过一个工具,以帮助教师判断作业是否是使用 ChatGPT 创建的。最终,OpenAI 收回了这个工具,如果你了解这些 GenAI 解决方案的基础数学和架构,我认为我们很快将达到一个这种事情实际上是不可能的阶段。

考虑一下当前市场上最先进的 LLM,包括 OpenAI 的 GPT-4 和 Anthropic 的 Claude 2。这些 LLM 可以生成看起来非常人性化的输出,这是因为其基础架构能够以令人震惊的精度评估单词之间的概率。在实际与人工意识之间小心翼翼地踩线时,人们不禁想知道,是否也可以将人类语言预测为单词之间的概率。在那种情况下,LLM 和人类的最终结果在概率上是无法区分的,因此没有工具能够明确地说,“这是由 GenAI 制作的;这是由人类制作的。”

总之,不要被那些声称可以区分由 AI 生成的内容和非 AI 生成的内容的工具所迷惑。 也许在更原始的 GenAI 解决方案中,它可以进行一定程度的评估,但我们可以说,像 GPT-4 这样的 LLM 已经超越了这种区分的程度。

可以 #4:GenAI 可以通过其他软件过程获得帮助。

虽然大多数人更熟悉通过像 ChatGPT 这样的用户界面与 LLM 互动,但几乎所有主要的 GenAI 参与者都通过 API 等方式提供编程解决方案。这意味着我们可以将 GenAI 技术嵌入到新的和现有的软件过程中。事实上,大多数 GenAI 初创公司正在做的正是这一点。具体来说,许多 GenAI 初创公司使用像 OpenAI 或 Anthropic 这样的提供商的 API 作为后端“引擎”来支持他们的 GenAI 需求。(这也是为什么你应该小心你的公司选择与谁合作,因为他们可能在幕后与第四方互动!)

再次强调,GenAI 应该始终小心幻觉。虽然 GenAI 可以生成代码片段以执行如从数据库查询特定信息等任务,但我个人绝不会依赖 LLM 来完成这样的任务。因为我们不能百分之百确定代码的正确性,所以依赖 GenAI 是不明智的。这并不是说你应该完全不将 GenAI 结合到你的软件系统中!仍然有很好的用例,你可以以仍然有益且保持谨慎的方式在编程解决方案中利用 GenAI。

不能 #4:GenAI 无法正确引用其自身的信息来源。

这是我经常被问到的问题,如果你到现在为止都在认真听讲,你会明白为什么这是不可能的。尽管 LLM 是在某一时间点上的信息上进行训练的,但并不是说它将所有词语之间的概率联系到具体的来源。LLM 所存储的仅仅是被称为权重和偏差的东西。无需深入 LLM 的底层架构,LLM 本质上由大量数学操作组成,在训练时,这些权重和偏差会更新以更贴近它所训练的信息。(实际上,如果我详细解释 LLM 的架构,你会惊讶于数学的基础到几乎令人感到神奇的程度,LLM 能够做得如此之多!)

这就是 LLM 的全部内容。没有直接联系到源信息,因此如果你要求它引用自己的来源,你最多只能得到虚假的回答。我实际上相信像 ChatGPT 这样的特定 LLM 已经被微调,以表示不可能揭示其来源,我认为这比虚假的回答要好。但是,获得虚假回答并非不可能。有那个臭名昭著的案例,一名律师使用 LLM 在法律案件中引用了一个来源,结果发现该来源是虚假的,最终是假的。小心不要陷入同样的陷阱!

可以#5:GenAI 可以极大地增强你的知识。

与 GenAI 可以作为编写代码的良好助手的观点类似,GenAI 也可以成为任何知识任务的好助手。我个人将 LLM(如 ChatGPT)用作一种“搜索引擎”来回答一般知识问题。例如,它在提供词汇定义方面表现出色,并且在你继续提出额外问题时,它能给出更详尽的回答。就像和一个非常有知识的朋友互动一样。虽然互联网搜索引擎可以提供最新和最相关的信息,但对话的互动流程是基本搜索引擎无法比拟的。

当然,我不应该再提醒你注意幻觉。虽然大型语言模型(LLMs)可能在历史事件和地点如古罗马方面更为有用,但如果你问它今天的天气,它肯定会给出一个虚假的答案。如果它提供了正确的答案,那是因为 LLM 以 RAG 方式被增强,而不是因为 LLM 能自行得出这些信息。另外,特别要小心使用 LLM 解决数学问题。记住,这些 LLM 是通过寻找单词之间的概率来进行训练的,所以在面对数学问题时,并没有进行实际的数学计算。最近几个月,像 ChatGPT 这样的流行 LLM 似乎被增强以更好地处理数学问题,但不清楚它们是如何做到的。在任何情况下,我仍然建议尽量避免用 LLM 解决复杂的数学问题。

无法做到 #5:GenAI 无法取代你的工作。(…还没有?)

啊,是的,大家最关心的问题!其实,如果我完全诚实的话,GenAI 可以用来减少人力,但我们绝对还未达到它会取代大多数人工作的地步。如果你仔细阅读了本帖关于信息检索的部分,你会发现 GenAI 无法取代所有人的工作的最大“缺陷”:GenAI 无法主动自学。GenAI 无法与您的 CEO 交谈并了解他们脑中的想法。GenAI 无法进入你的脑袋,理解你在想象中构建的网站的所有细微差别。以这种方式,我认为 GenAI 就像一个完全天真的天才:你可以让它做很多事,但你必须直接而极其具体地与它沟通,以获得最理想的结果。

自然的后续问题是:AI 是否会达到能够完成这些事情的程度?我们将这一超级智能的下一个层次称为人工通用智能(AGI),这也是 AI 安全倡导者对 AI 进展表示关注的原因。目标是安全地使人类利益与 AI 利益对齐。不再深入讨论这一话题,重点是我们今天还未达到这一点,也真的不清楚我们何时会到达奇点——即我们达到 AGI 的那一点。如果你了解 AI 研究的历史,你会很快发现人类在预测这一点上并不擅长。 😂

我希望你能从这篇文章中更好地了解 GenAI 可能的优缺点。这是一项真正令人惊叹的技术,但并不是适用于所有情况。我还要强调的一点是:GenAI 并不是唯一的 AI。GenAI 的当前形式最早出现在 2017 年,但我们还有许多其他形式的 AI 也能有效完成任务。就像你不会租车走十英尺一样,你应该始终尝试调整解决方案以满足业务需求。GenAI 只是我们工具箱中的另一个工具,尽管是一个非常酷的工具! 😃

我从第一次 R 编程活动中学到的五件事

原文:towardsdatascience.com/five-things-i-learned-from-my-first-r-programming-event-3fceb3caa6a1?source=collection_archive---------6-----------------------#2023-05-11

从 SatRDays London 学到的关于 R、数据科学和吸引观众的经验

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Rory Spanton

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 5 月 11 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Teemu PaananenUnsplash 提供

上个月,我做了一件以前从未做过的事情。我参加了一个关于数据科学的线下活动——具体来说,是在 R 编程语言中进行数据科学。

我对会议并不陌生。作为心理学研究员,我参加并在许多相关领域的会议上做过报告。但尽管我是一个长期的 R 爱好者,我从未有机会参加关于数据分析的非学术会议。因此,当我有机会参加 SatRDays 伦敦,这个为期一天的专注于 R 的活动时,我毫不犹豫地报名参加了。

这是一个很棒的决定。我学到了很多关于 R 及其在许多行业中的应用的知识,其中一些是我以前从未听说过的。我还与许多对 R 像我一样痴迷的人交流,这真的很令人愉快。

这是我从 SatRDays 学到的五个最强大的东西之一。

1. R 的用途比我想象的要多

以为你知道 R 能做所有事情?再想想。

我有时听到数据科学家将 R 贬低为一个只有少数用途的专业语言。他们说,如果你不从事生物信息学、学术研究或硬核统计学,你所在的领域里没有人使用 R。Python 是一个通用的、“万能”的语言,你应该改用它。

在某种程度上,这是正确的——像 Python 这样的其他语言比 R 更广泛使用,并且在自身领域中也很有价值。但这并不意味着 R 不能在各个领域完成许多重要的任务。

SatRDays 的讲座范围令人难以置信。每位讲者来自不同的公司,展示了他们在行业中如何使用 R 进行数据分析的新颖而有趣的方法。

我听了数据记者、金融审计师、互联网性能分析师、空气质量专家以及更多人分享他们与 R 的经验。作为一个学术人员,这些领域中的一半在会议之前甚至不在我的雷达上。听到这些人的讲述让我意识到 R 在各种商业环境中的广泛用途。

当然,R 语言的工作机会没有 Python 语言那么多。但不要让任何人告诉你学习 R 是把自己逼入绝境。越来越多的公司开始采纳 R,并且有许多新的应用场景值得期待。

2. 良好的 R 代码将计算与操作分开

这是我从 Russ Hyde 关于良好编码实践的讲座中学到的一个提示。对于经验丰富的开发者来说,这可能是熟悉的,但对我来说却是新颖而有用的。

在编程时,通常最好将你的代码分成用户定义的函数。这有助于避免脚本中的重复,并确保代码是可重用和易于维护的。但,有些将代码拆分成不同函数的方法比其他方法更好。

比如,考虑一个简短的脚本,它读取一些数据,清理这些数据,然后将其写入一个新文件。这个脚本包含许多步骤,但我们可以将每个步骤分为两类:计算和操作。

read_csv("sales_data.csv") %>%
  select(date, transaction_id, category, item_price) %>%
  filter(date == "2023-05-10") %>%
  group_by(category) %>%
  summarise(day_turnover = sum(item_price)) %>%
  write_csv("sales_summaries/day_turnover_2023-05-10.csv")

计算是一步步的操作,给定一定的输入,每次都会返回相同的输出。过滤数据集和计算摘要统计量等操作就是很好的例子。因为它们返回可预测的结果且没有任何副作用,所以很容易编写自动化测试来确认它们的正常工作。

对比起来,动作要难测试得多。它们会产生副作用,比如将数据写入文件,这些副作用更难控制。它们还可能受到全局环境中的随机数生成器(RNG)或其他变量的影响。这使得测试它们变成一项挑战,通常需要使用虚拟文件或受控环境。

提示是:尽可能地将动作与计算分离。

这里有一个使用我之前介绍的代码的例子。与其在一个块中包含动作和计算,不如将它们分成两个函数。所有读写数据的动作都包含在一个函数中,而所有的计算都在另一个函数中。

calculate_turnover <- function(sales_data, day) {
  sales_data %>%
    select(date, transaction_id, category, item_price) %>%
    filter(date == day) %>%
    group_by(category) %>%
    summarise(day_turnover = sum(item_price))
}

action_turnover <- function(day) {
  read_csv("sales_data.csv") %>%
    calculate_turnover(day) %>%
    write_csv(paste0("sales_summarise/day_turnover_", day, ".csv"))
}

action_turnover("2023-05-10")

这使得代码整洁而分隔明确。测试计算过程非常简单,因为它们与动作的任何副作用都被清晰分隔开来。这种分离也使我的动作更容易测试。

“哦,是的,他们在游戏开发中也经常这样做”,我向一位同事描述了这种方法后,他这样回答道。如果你有软件开发背景,你很可能已经知道这一点。但是数据行业的人员来自各行各业,有时可能从未学习过这些技巧。虽然我自学了一些良好的编码原则,但对我来说,这是一个新的领域,今后我会更多地加以利用。

3. 严格的测试是必不可少且被期望的。

谈到测试,这是整个会议的一个常见主题。

Vyara Apostolova 和 Laura Cole 在国家审计办公室的一次讲话中重点讨论了测试和检查。她们的工作涉及接收重要的政府模型,并在 R 中复现这些模型。在此过程中,她们会细致地测试每个模型中的每个假设,检查错误和差异。

这可能需要数年时间才能完成。但是,这种细致的工作对于发现昂贵的错误至关重要。她们揭示了,由于在金融模型中的错误导致的不必要的费用浪费,他们的工作已经节省了数百万英镑。这一切都是因为他们在严格的框架内测试、检查和评估他们编码的一切。

在其他讲话中,观众的问题经常与测试相关。如果有人展示了使用 R 进行某种新的酷炫方法,人们想知道的第一件事就是如何测试它。

尽管 R 在我们的学术教学和实践中是内置的,但测试却被高度低估。从外部视角来看,SatRDays 让我意识到,对于数据专业人士来说,拥有健壮的自动化检查工作流程是多么重要。

无论你是已经从事数据工作还是试图进入这个行业,测试都可以巩固你工作的价值。离开 SatRDays 后,我计划在几个月内找工作之前提高我的软件测试技能。

4. 学习 R 可能会很有趣。

我以前写过关于学习 R 的乐趣。但在 SatRDays 之前,我从未因数据科学演讲而大笑过。

如果我必须从整天的演讲中选择一个最喜欢的,那就是Andrew Collier的“整理世界的助手”。在这个演讲中,Andrew 介绍了一些不太为人知的 tidyverse 函数,以及它们如何补充更常用的函数。

在演讲中解释代码的工作原理是一个艰巨的任务。你需要在提供信息和避免让观众被技术细节困住之间找到平衡。SatRDays 的所有演讲都实现了这种平衡,但 Andrew 以突出的幽默和风格做到了这一点。

他的演讲充满了流行文化的引用和恰到好处的笑话。这些不仅有趣,还将新概念与观众已经知道的信息联系起来。

在演讲中使用幽默意味着让自己暴露在外。让观众笑一笑是一种特别温暖和令人满意的体验。当你的笑话在寂静的房间里回响时,那种感觉则完全相反。这就是演讲中的高潮和低谷。

话虽如此,幽默在技术演讲中是一个很好的工具,即使它并不总是有效。与其成为娱乐性的分心,它如果用得当,甚至能使你的解释更出色。往后,我会尝试在我的演讲中加入更多幽默,以使其更具特色。

5. R 社区独一无二。

R 社区非常棒。

这一点对于了解我人品的任何人来说都不应该感到惊讶。我鼓励在学习 R 时积极参与社区互动,甚至在选择使用哪些软件包时也是如此。

## Tidyverse 与 Base-R:如何为自己选择最佳框架

最受欢迎的 R 编程方法的优缺点。

towardsdatascience.com

但我从未与这么多 R 用户和专业人士待在同一空间里过。我在一天开始时不认识任何人,所以我希望能找到至少几个友好的人聊聊天。说实话,我有点紧张。

我不必担心。我遇到的每个人都很友好、平易近人且有趣。我不是一个外向的人,也不擅长社交,但我在所有休息时间都站着和人交谈。我知道这在所有事件中并不是必然的——我曾经有几次孤独的午餐没有伴侣,所以在这里尤其感到高兴。

R 社区也在多样化。Ella Kaye 和 Heather Turner 讨论了她们在让代表性不足的群体参与 R 语言维护方面的持续工作。目前,大多数主要的语言贡献者是接近职业生涯末期的西方男性。为了使 R 语言保持运作并与时俱进,重要的是将接力棒交给来自全球的更多样化的贡献者。Ella 和 Heather 分享了她们正在设置的各种倡议和活动,所有这些都听起来是前进的有希望的步骤。

我很高兴看到这种多样性在观众中得到了体现。虽然我算得上是众多戴眼镜的白人男性中的一员,但也有很多其他性别、特征和背景的人。我感觉每个人都被包括在内,组织者也积极促进了这一点。

如果这里有一个教训,那就是你不应该害怕与人建立联系,特别是在 R 社区中。他们可以为你提供关于工作、公司和技术的宝贵见解,这些都能推动你的职业发展。而且,至少在我的经历中,他们真的很友善。

参加一个 R 事件是一次很棒的经历,我希望以后能再去一次。我不仅了解了我最喜欢的编程语言,还认识了使用这门语言的人和相关行业。可以说,我将从 SatRDays 中获得很多收获,并在未来几个月内运用我新获得的知识。

如果你在阅读完后做些什么,请找到一些以你喜欢的数据科学技术为中心的活动,看看你是否可以去参加其中一个。组织 SatRDays 的公司 Jumping Rivers 有一个 R 事件列表供你查看。像RLadies这样的其他组织也经常在全球范围内举办面对面的聚会。

参加一个新的活动可能会让人感到紧张,但如果它与你及你的兴趣相关,通常是值得的。

想要将我所有关于 R 编程、数据科学等的文章直接送到你的邮箱吗? 点击这里订阅

要获取我在 Medium 上的所有故事的完整访问权限,请通过此链接注册会员。

处理大型动作空间的五种方法

原文:towardsdatascience.com/five-ways-to-handle-large-action-spaces-in-reinforcement-learning-8ba6b6ca7472

动作空间,尤其是在组合优化问题中,可能会变得庞大无比。本文讨论了处理这些问题的五种策略。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Wouter van Heeswijk, PhD

·发表于 Towards Data Science ·14 分钟阅读·2023 年 8 月 18 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

还有……行动![照片由 Jakob Owens 提供,来自 Unsplash]

处理大型动作空间仍然是强化学习中的一个相当开放的问题。研究人员在处理大型状态空间方面取得了重大进展,卷积网络和变压器是一些近期的高调例子。然而,存在三种所谓的维度诅咒:状态、结果和动作[1]。到目前为止,后者仍然是相对未被充分研究的。

尽管如此,处理大型动作空间的方法正在不断增长。本文介绍了处理后者的大规模的五种方法,特别关注在组合优化问题中经常遇到的高维离散动作空间

回顾一下:维度灾难的三种诅咒

需要快速回顾维度灾难的三种诅咒。假设我们将手头的问题表达为一个 Bellman 方程 系统,请注意有 三组需要评估——实际操作中以嵌套循环的形式——每组可能都大得难以承受:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Bellman 方程要求对于每个状态-动作对 (s,a)∈S×A,必须评估所有潜在的结果 s’∈S’,这迅速使得对这个随机优化问题的枚举在计算上变得不可行。

从根本上讲,强化学习是一种蒙特卡罗模拟,通过采样随机过渡而不是枚举所有可能的结果。根据大数法则,样本结果最终应该促进收敛到真实值。通过这种方式,我们将随机问题转化为确定性问题:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

值函数近似提供了一个确定性优化问题,在评估状态-动作对时,我们不需要评估整个结果空间,而只是一个单一的下游值。

这种转化使我们能够处理大的结果空间。要处理大的状态空间,我们必须能够对以前未见过的状态进行概括。常见的方法有特征提取或聚合,这也是研究的主要关注点。

由于我们可以评估一个对应于状态-动作对的单一值——而不是评估所有对应的结果——因此评估数百或数千个动作通常不是问题。对于许多问题(例如象棋、视频游戏),这就足够了,不需要进一步对动作空间进行近似。

尽管如此,如果动作空间仍然过大,我们不能使用与结果和状态空间相同的解决方案方法。仅仅对一个动作进行采样——就像我们对结果做的那样——并不能保证它是有效的,而且绕过了学习和使用智能决策策略的概念。像在状态上应用的概括方法也不起作用,因为我们最终需要一个可以应用到环境中的具体动作。因此,我们需要其他的解决方案。

什么是“大的”动作空间?

在深入解决方案之前,我们首先需要明确什么是“大的”动作空间。为了概括,我们用一些向量a=[a_n]_n∈N表示动作,其中N表示动作的维度。

“大的”这个术语可以用多种方式定义:

  • 动作数量: 一个动作可以假设的值的数量。请注意,这个数量可能是无限的,例如整个整数域。显然,对于基于向量的动作,动作数量增加得比基于标量的动作快得多。考虑一个 9 维向量,其中每个元素可以取 10 个值。那么,总的动作空间已经达到 10⁹ = 10 亿个动作!

  • 连续决策变量: 如果动作向量包含一个或多个连续变量,则动作的数量在定义上是无限的。连续变量在例如机器人学中很常见,其中关节运动由实值数表示。

  • 维度:对于基于向量的决策,维度(元素数量)有巨大的影响——每增加一个元素,复杂性就呈指数级增加。特别是当动作是排列时,基于向量的决策空间会迅速变得非常大。

  • 可枚举的:按照传统标准,一个有十亿个动作的动作空间可能被认为是‘大’的;你肯定不想在每次迭代时评估十亿个动作。不过,这样的动作空间仍然可以在计算机内存中被枚举和存储。相比之下,其他问题的动作空间巨大到我们根本无从枚举。如果动作空间随着问题规模的增长呈指数级增长,它们会迅速变得不可处理。

本文的重点将放在多维离散动作空间上。这些在现实问题中常常出现,导致组合优化问题的规模迅速膨胀。例如,考虑运筹学问题:管理出租车队、大量物品的库存补货政策、在船上堆放集装箱等。

让我们把我们面对的问题的规模变得更加具体。

考虑在一个港口管理集装箱运输,每个集装箱的特点包括(i)目的地,(ii)到期日期和(iii)到达日期。假设我们有 10 个潜在的目的地、20 个到期日期和 5 个到达日期。那么就有 10205 种独特的集装箱类型,这转化为一个 1000 维的动作向量。考虑到一些不同的运输选项和一个适度大的集装箱堆场,创建一个比地球上沙粒还多的动作空间并不难,或者任何你想做的类比。

另一个例子?

考虑一个包含 1000 个项目的推荐系统,我们向用户推荐两个项目。因此,有 1000*999(即约 100 万)种项目组合可以推荐。显然,推荐 3 个、4 个或 5 个项目会使动作空间呈指数级增长。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于向量的动作空间通常会随着动作向量维度的增加而呈指数级增长。组合优化问题以生成非常大的动作空间而臭名昭著,即使对于看似简单的实例也是如此。[作者提供的图片]

I. 基于演员的方法(仅适用于连续动作空间!)

基于价值的方法,如 Q 学习,在连续动作情况下会崩溃,需要计算无限数量的 Q 值。可以对动作空间进行离散化,离散化的粒度决定了动作空间大小和准确性之间的权衡。例如,将方向盘的转动(连续空间)表示为 360 个离散度数,或者 36、3600 等。然而,对于基于向量的决策,这种离散化很快就会失效。

进入基于演员的方法[2]。这些方法通常使用神经网络,将状态作为输入,输出使从概率分布中采样的参数。例如,输出节点可以是高斯分布的均值和标准差,我们可以从中采样连续动作。

向量化决策的泛化相当简单,每个向量元素由一个单独的分布表示(即,相对于向量维度的线性缩放)。因此,也高维连续动作空间可以被高效处理。

关键在于演员网络直接将状态映射到动作,无需检查每个状态-动作对的值。如果需要——如为了减少方差——这些演员方法可以扩展为演员-评论员网络,其中评论员网络是一个扩展,输出生成的状态-动作对的值。对于连续动作空间,演员-评论员方法如 PPO 仍然是最先进的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于连续动作空间,演员网络高效地生成和评估动作,例如,通过输出高斯分布的均值和标准差来进行采样。扩展到多维动作向量时,统计参数对应于各个向量元素,从而使输出层的规模与动作维度线性相关。[image by author]

不幸的是,基于演员的模型在离散动作空间中扩展效果不佳。在这种情况下,每个输出节点代表单个动作的选择概率。显而易见,如果我们有数十亿个离散动作,这种方法不会很好地扩展。因此,对于离散动作空间,我们需要想出其他解决方案。

II. 数学编程

许多现实世界的决策问题可以通过凸动作空间来表达。凸性是一个非常强大的属性。你真的得亲眼见到才能相信。像 CPLEX 和 Gurobi 这样的解算器能够极其高效地处理大型决策问题,即使它们涉及到数千个决策变量,涉及到数百万甚至数十亿的动作[1]。

尤其是对于线性问题——许多实际问题(大致上)都是如此——解算器在过去几十年中已被高度优化。注意,分段线性函数可以用来近似非线性函数,例如,神经网络中的 ReLUs [3]。连续决策变量也没有问题。事实上,它们通常更容易处理。

数学程序也非常适合处理约束。执行显式可行性检查以构建动作掩码可能很繁琐。约束方程有效地筛选出所有缺失/不可行的动作。

尽管通过数学规划的规模提升可能非常显著,但对于不断增长的动作空间没有理论保证。计算工作仍可能随着动作空间的增大而呈指数增长。不过,在实践中,相较于优化,扩展性通常非常可观。

根据问题结构,数学规划的效果可能通过列生成或贝尔曼分解等方法得到增强。强大的实现可以将性能提升几个数量级。缺点是这些算法需要对问题结构有深入的了解,并且需要大量的设计工作。

## 使用线性规划提升强化学习算法

大型和高维的动作空间通常是强化学习中的计算瓶颈。制定…

towardsdatascience.com

III. 启发式方法

启发式方法对动作空间施加搜索或决策规则,绕过对完整动作空间的评估,以提高速度和效率。

动作空间缩减

更高效地搜索动作空间的最简单方法之一是切割其部分区域。显然,这存在切割高质量区域的风险。然而,可以利用领域知识定义合理的决策规则,例如:

  • 永远不要派遣填充率低于 70%的卡车

  • 马里奥应该始终向右移动

  • 始终建议在有人购买咖啡机时使用咖啡过滤器

可以看出,这很棘手。有时,马里奥可能需要向左移动以进行跳跃。也许我们需要等很久才能在安静的周内达到 70%的填充率。也许客户对咖啡豆研磨机而非过滤器更感兴趣。人类的专业知识、逻辑和直觉非常强大,但也有明显的缺点。

尽管启发式缩减可能显著降低计算负担,但其设计高度特定于问题,当问题实例发生变化时不一定具有可扩展性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过切割动作空间的区域(例如,添加约束),搜索可能变得不那么繁琐。然而,不恰当的切割可能会删除高质量的动作。[图片由Sdo提供,来源于维基百科]

元启发式方法

基础启发式方法通常依赖于人为定义的决策规则,这些规则可能是次优的。实际上,许多问题结构超出了人类的认知极限。为了自动化地寻找好的解决方案,元启发式(例如,模拟退火、遗传算法)指导动作空间的搜索,例如通过偶尔接受非改善的动作或重新组合它们[4]。

元启发式的搜索过程通常是与问题无关的,尽管不可避免地会有需要用户设计的特定于问题的元素。参数调整也是必要的——而我们在强化学习中已经有了大量这样的参数——同时,理论保证被放弃了(至少在实际应用中)。

尽管有缺点,元启发式方法在许多极具挑战性的优化问题中已证明了它们的实力,而可调搜索和启发式设计的强大组合确保它可以适应几乎任何动作空间。特别是对于混乱的现实世界问题,元启发式方法可能是处理大规模动作空间的最佳选择[1]。

数学启发式方法

一种特殊的启发式方法融合了 元启发式和数学编程 [5],旨在利用两者的优势。回顾一下,数学程序特别适合利用凸结构和大量决策变量,而启发式方法可以用来控制搜索空间和过程。如果动作空间如此之大,以至于即使是数学编程也不能在可接受的时间内返回解,将其与启发式方法结合可能是一个解决办法。

通用启发式算法实现包括如局部分支、邻近搜索和变量固定等技术。例如:我们可以启发式地减少一个动作空间,然后用数学编程搜索该动作空间。或者,我们可能用数学编程解决一个高层次的程序(例如,将客户分配给车辆),并通过启发式方法填充细节(例如,启发式生成路线)。有很多角度和不同的子程序分布。

数学启发式方法可能非常强大,规模远超纯数学编程——但牺牲了性能保证——但也可能设计上相当复杂。然而,当做到正确时,数学编程与启发式方法的结合可能是非常富有成效的。

IV. 连续到离散的映射

如前所述,基于演员的方法可以通过提供逐元素输出有效地处理多维连续动作空间。如果我们能对离散空间做到这一点就好了……

幸运的是,许多离散问题都有一些潜在的连续表示。例如:我们可能无法运输5.742…个集装箱,但如果这样的实数解能给出好的解决方案,那么运输 5 或 6 个集装箱可能效果很好。

如果所需的连续表示存在,我们可以部署一个演员网络来获得所谓的原型动作(离散动作的连续近似),然后将其转换为“类似”的离散动作。这种从连续原型动作到离散动作的转换并不一定简单。是时候深入研究一些实现这一目标的映射了。

MinMax

将连续动作转换为离散动作的最直接方法是简单地四舍五入元素。例如,连续的原型动作 [3.67…,1.22…,2.49…] 可以转换为 [4,1,2]。这样的映射可以稍微调整,但这就是一般的想法[6]。由于我们只需要采样一个连续动作并应用一个转换,这种方法可以扩展到极大的动作空间,超出枚举的范围。

如果连续量与其离散对应量之间有意义的结构关系,那么直接映射可能效果很好。例如,在库存管理中,将订购数量四舍五入到最近的整数是完全合理的。

不幸的是,并非所有离散动作空间都展示出如此干净的连续表示。假设我们训练一个推荐系统,整数表示要推荐的项目。如果 a=3 代表冰箱,而 a=4 代表搅拌机,最合适的邻居与原型动作 3.48… 相距远远不明显。让我们检查一些更先进的映射。

k-最近邻

k-最近邻(knn)方法也从一个原型动作开始,但随后在离散动作的邻域中搜索[7]。为了有效地做到这一点,整个动作空间被先验编码,这样我们可以快速找到我们可能定义的每个连续动作的 k 个最近邻。

在识别邻居后,knn 随后评估它们的 Q 值以选择最佳邻居。k 的值可以调整,更多的邻居可能识别更好的动作,但代价是更多的计算工作和越来越多的离策略动作。

knn 的优点和缺点都在于其对动作空间的先验枚举。预处理阶段使得邻居的高效查找成为可能,但也要求空间是可枚举的。

学习的动作表示

对于连续原型动作,“最接近”的离散邻居可能不是一个明显的选择,它们之间的关系在动作空间中可能有所不同。与其拥有固定的用户定义邻居,我们还可以学习将连续动作映射到离散动作的动作表示。在一个单独的监督学习阶段,神经网络被应用于学习每个离散动作的量身定制表示[8]。

学习的动作表示提供了强大的泛化能力,消除了对合适邻居的人工推理的需求,允许应用于复杂的动作空间结构。缺点是必须为每个离散动作学习动作表示,给 RL 算法增加了额外的复杂性。每个表示必须存储在内存中,因此该方法也限制在可枚举的动作空间中。此外,学习的动作表示可能在处理高维动作向量时遇到困难,因为目标向量变得越来越难以学习。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

学习的动作表示尝试学习嵌入,将连续的原型动作映射到最合适的离散邻居,具体取决于动作空间的结构 [照片由 Michael Busch 提供,来自 Unsplash]

动态邻域搜索

MinMax 方法在其映射中简单直观,但最接近的邻居可能不总是最合适的。像 knn 和学习的动作表示这样的算法处理复杂的动作空间,但需要为每个动作提供显式的嵌入,因此只处理可枚举的动作空间。通过动态邻域构建 [9],离散邻域到原型动作被即时生成和评估,从而扩展到超出枚举的动作空间。遵循以下两个步骤:

  • 扰动:通过扰动方案有效地生成邻域,每次改变一个元素,使得计算工作量与动作向量的维度成线性关系。

  • 模拟退火:该扰动方案有效地生成离散动作的邻域,但省略了需要多元素扰动的动作。为了恢复这些动作(如果需要),模拟退火会探索新邻域,针对具有最高 Q 值的动作。

类似于 MinMax,动态邻域搜索需要一个特定的动作空间结构,其中小的扰动生成具有相似奖励的邻域。这种结构在许多实际问题中(例如,时空表示)可能存在,但该方法不能推广到所有离散动作空间。最后,每次迭代的运行时间较长,因为需要每次生成和评估动作,尽管它可能找到更优的动作。

V. 因式分解

因式分解(或称为分解)是一种方法,将动作分组并为每个分组找到动作表示,使得这些表示更易于学习。

因式分解方法的一个例子是二值化,其中所有行动都以二进制代码表示 [10,11]。对于每一位,学习一个相关的价值函数/子策略,指数性地减少评估次数。对于许多方法来说,完全枚举行动空间是先决条件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二值化可以在超立方体上表示,我们可以对每一位进行结构化搜索 [图像来源:Vlad2i 通过 Wikimedia]

一种特殊的因式分解变体使用层次化或多智能体推理来因式分解行动空间 [12]。考虑管理一队出租车,其中每辆出租车的分配由单独的向量元素表示。对这支车队进行集中控制会产生大量的组合,这些组合都需要进行评估。

相反,我们可以在个体智能体的层面进行推理,在其局部环境中解决一个更简单的分配问题。鉴于上下文,这种多智能体视角可能是合理的,因为在城市另一边的出租车决策对当地决策的影响可能微乎其微。

尽管计算上比集中控制要容易得多,但得到的解决方案可能是次优的,例如,两辆出租车可能会接受同一个客户。为了确保行动的可行性并可能使用全局系统知识来改进它,需要一个事后同步步骤

多智能体或层次化 RL 方法的设计可能具有挑战性,其适用性很大程度上取决于具体问题。然而,如果操作得当,分解可能会产生高质量的全局解决方案,同时在局部层面进行更快速的推理。

TL;DR

这篇文章相当长,因此如果你直接滑到最后,我不会怪你。这张表总结了最突出的要点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对处理大行动空间的五种策略的简要描述,包括它们的优缺点 [图像来源:作者]

作者的其他强化学习文章:

## 近端策略优化(PPO)解释

从 REINFORCE 到连续控制中的首选算法的旅程

[往数据科学的方向看 ## 强化学习中的策略梯度解释

了解基于似然比的策略梯度算法(REINFORCE):直觉、推导、……

[towardsdatascience.com ## 强化学习的四种策略类别

[towardsdatascience.com

参考文献

[1] Powell, W. B. (2022). 强化学习与随机优化:序列决策的统一框架。John Wiley & Sons。

[2] Sutton, R. S., & Barto, A. G. (2018). 强化学习:导论。MIT Press。

[3] Van Heeswijk, W. & La Poutré, H. (2020 年 12 月). 线性离散动作空间中的深度强化学习。在 2020 IEEE 冬季模拟会议 (WSC) (第 1063–1074 页)。

[4] 维基百科贡献者 (2023). 元启发式算法。 en.wikipedia.org/wiki/Metaheuristic

[5] Fischetti, M., Fischetti, M. (2018). 数学启发式。在: Martí, R., Pardalos, P., Resende, M. (编) 启发式手册。Springer, Cham。

[6] Vanvuchelen, N., Moor, B. de & Boute R. (2022). 使用连续动作表示来扩展用于库存控制的深度强化学习。SSRN, 2022 年 10 月。dx.doi.org/10.2139/ssrn.4253600

[7] Dulac-Arnold 等 (2015). 大规模离散动作空间中的深度强化学习。arXiv 预印本 arXiv:1512.07679。

[8] Chandak, Y. 等 (2019). 强化学习的动作表示学习。在国际机器学习会议上,页码 941–950。PMLR, 2019。

[9] Akkerman, F., Luy, J., Van Heeswijk, W., & Schiffer, M. (2023). 通过动态邻域构建处理大规模离散动作空间。arXiv 预印本 arXiv:2305.19891

[10] Dulac-Arnold 等 (2012). 快速强化学习与

使用误差校正输出代码的大动作集的 MDP 分解。在联合欧洲机器学习与数据库知识发现会议上, 180–194。

[11] Pazis J. & Parr, R. (2011). 大动作集的广义价值函数。在第 28 届国际机器学习大会 (ICML-11) 论文集中, 1185–1192。

[12] Enders, T., Harrison, J., Pavone, M., & Schiffer, M. (2023). 用于按需自主移动系统的混合多智能体深度强化学习。在 动态与控制学习会议 (第 1284–1296 页)。PMLR。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值