TowardsDataScience-2023-博客中文翻译-二-

TowardsDataScience 2023 博客中文翻译(二)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

每个数据科学家都应该刻在脑海中的 5 个问题

原文:towardsdatascience.com/5-questions-every-data-scientist-should-hardcode-into-their-brain-3948e215750f

我是否在解决正确的问题?

Shaw TalebiTowards Data Science Shaw Talebi

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

--

Tingey Injury Law Firm 提供的照片,来源于 Unsplash

尽管有所有的数学和编程,数据科学不仅仅是分析数据和建立模型。归根结底,数据科学的关键目标是解决问题

然而,问题在于,大多数数据科学项目开始时,我们很少有明确的问题。在这些情况下,数据科学家的角色不是拥有所有答案,而是提出正确的问题

在本文中,我将详细介绍每个数据科学家都应该刻在脑海中的 5 个问题,使问题发现成为第二天性。

锤子时刻

当我在研究生院开始我的数据科学旅程时,我对这一学科有一个天真的看法。也就是说,我过于专注于学习工具和技术(例如 LSTM、SHAP、VAE、SOM、SQL 等)

虽然技术基础对于成为成功的数据科学家是必要的,但过于关注工具会产生“锤子问题”(即当你有一把非常好的锤子时,一切看起来都像钉子)。

这通常会导致一些智力刺激但实际无用的项目。

问题 > 技术

我的观点直到毕业并加入一家大型企业的数据科学团队时才完全成熟,在那里我能够向那些比我早几年(如果不是几十年)的人学习。

关键的教训是专注于问题而非技术的重要性。这意味着在编写任何一行代码之前,深入理解业务问题。

作为数据科学家,我们通常不会解决自己的问题,我们通过与客户和利益相关者的对话来获得这些理解。正确把握这一点很重要,因为如果你做不好,你可能会花费大量时间(和金钱)解决错误的问题。这就是“问题发现”问题派上用场的地方。

5 个问题发现问题

6 个月前,我离开了我的企业数据科学工作,成为了一名独立的数据科学顾问(为了资助我的创业项目)。从那时起,我对破解这些早期阶段的“发现”对话产生了强烈的兴趣。

我提高这一技能的方法有两个方面。首先,采访经验丰富的数据自由职业者,了解他们的最佳实践(我采访了 10 位)。其次,尽可能多地进行发现性通话(我进行了大约 25 次)。

这里列出的问题是我之前提到的所有经验的总结。虽然这绝不是一个完整的列表,但这些是我发现自己反复询问的问题。

1)你想解决什么问题?

虽然(理论上)这应该是这份清单上的唯一问题,但(不幸的是)实际情况并非如此。很多时候,客户并不明确自己需要解决的问题(如果他们明确,可能就不会找咨询师了)。即使他们明确了,我通常也需要跟上步伐,以更好地理解业务背景。

这个问题在两种情况下都很有帮助,因为(理想情况下)客户的回答会引发后续问题,从而让我更深入地了解他们的世界。例如,他们可能会说:“我们尝试过使用 OpenAI 创建一个自定义聊天机器人,但效果不好。”

对此我可能会问:“这个聊天机器人是用来做什么的?” 或者 “是什么让你觉得结果不好?”。

2)为什么……?

什么”的自然后续问题是“为什么”。这是你可以问客户的最强有力的问题之一。然而,它也可能是最难提出的问题之一

为什么”问题往往会让人感到防备,因此拥有多种表述这种问题的方式会很有帮助。这里有一些例子:

  • 这对你的业务(你的团队)为什么重要?

  • 你为什么现在想要解决这个问题?

  • 解决这个问题对你的业务意味着什么?

  • 这如何与业务的更大目标相适应?

  • 你为什么想用人工智能来解决这个问题?

这个问题(或其任何变体)是一种非常有效的方式来获得客户的背景信息,这应该(再次)激发后续问题。

为了继续之前的例子,客户可能会说,“我们有几个支持票据,想要自动分类为 3 个优先级别,我们认为 AI 聊天机器人是解决这个问题的好方法。” 这为之前的“我们尝试制作了一个定制聊天机器人”的回应提供了更多背景。

我们在做什么?”和“我们为什么要这样做?”是商业中最基本的两个问题。因此,擅长提出“什么”和“为什么”可以让你走得更远。

3) 你的梦想结果是什么?

我喜欢这个问题,因为它(有效地)结合了“什么”和“为什么”的问题。这使客户可以以直接问到时可能无法传达的方式谈论他们对项目的愿景。

学习新事物时,我通常需要多次尝试才能真正理解。同样,我发现为了真正找到客户问题的根源,我需要在对话中以不同的方式问“什么”和“为什么几次

这让人联想到丰田的“5 个为什么”方法,用于找出问题的根本原因。尽管这是在制造业背景下开发的,但同样适用于数据科学中的问题解决。

这里两个相关的问题是:成功的标准是什么?我们如何衡量它? 这些比“梦想结果”要更具实用性,但有助于从询问“什么和为什么”过渡到“怎么做?

4) 到目前为止你尝试了什么?

这个问题开启了解决方案的路径。它通过揭示项目的更多技术细节来实现这一点。

例如,这通常让我对谁将编写代码有个很好的了解。如果他们已经构建了一些基本的 POC,那么客户(及其团队)可能会承担大部分工作。如果他们从头开始,那可能是我或我网络中的分包商。

在这个客户还没有做任何事情的第二种情况下,可以提出一些其他问题。

  • 现有的解决方案是什么?

  • 你现在是如何解决这个问题的?

  • 其他人是如何解决类似问题的?

5) 为什么是我?

我从大师级谈判专家Chris Voss那里获得了这个问题,他将其框架化为揭示人们动机的有效方法。

通常,这会引发有关是什么导致他们找到你的额外背景信息,以及他们****如何看待你融入项目,这对定义下一步非常有帮助。

然而,有时,人们对这个问题没有很好的答案,这可能表明他们实际上并不想与你合作,并且隐藏了他们的真实意图(例如,他们想要免费的咨询或竞争性报价)。

一个关键的教训

过去几个月对我来说一个重要的教训是学习这些问题(即深深地记在脑海中但随后忘记它们

目标是让这些问题在你与人交谈的过程中自然地在脑海中形成。虽然这需要经历不少尴尬时刻,但只有通过实践才能够发展起来的(别担心,我也在不断练习)。

让我受益的是以学习为目标进入这些对话。这意味着保持好奇心提问倾听(比说话)更多。

结论

虽然数据科学需要技术技能,但如果没有对问题的清晰理解,这些技能无法提供太多价值。这就是为什么发展识别和理解客户业务问题所需的沟通技能至关重要。

在这里,我分享了发现问题的 5 个基本问题。虽然这不是一个完整的列表,但我希望对承担更多客户面对面角色的数据从业者有所帮助。

如果你有任何补充,请留下评论 😁。

## 每个数据科学家都应该考虑自由职业的 5 个理由

这可能会让你感到不舒服,但对你有好处

[towardsdatascience.com

资源

联系: 我的网站 | 预约电话 | 问我任何问题

社交媒体: YouTube 🎥 | LinkedIn | Twitter

支持: 请我喝杯咖啡 ☕️

shawhin.medium.com/subscribe?source=post_page-----3948e215750f-------------------------------- [## 免费获取我写的每一篇新故事

免费获取我写的每一篇新故事 P.S. 我不会与任何人分享你的电子邮件。通过注册,你将创建一个…

shawhin.medium.com

提升您的 MLflow 模型实验的 5 个快速技巧

原文:towardsdatascience.com/5-quick-tips-to-improve-your-mlflow-model-experimentation-dae346db825?source=collection_archive---------11-----------------------#2023-03-13

使用 MLflow Python API 来驱动更好的模型开发

Matt CollinsTowards Data Science Matt Collins

·

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

--

MLflow 是加速机器学习模型开发过程的绝佳工具,其强大的实验组件使数据科学家能够记录最佳算法和参数组合,并迅速迭代模型开发。

本博客旨在展示如何充分利用 MLflow 实验。我们将重点介绍start_run()及其参数,这些参数可以增强您的实验过程。此外,我们还将介绍search_runs()函数,它提供了您实验历史的广泛视图,并在分析中提供更大的灵活性。

如果你是 MLflow 的新手,我建议在进入此博客之前,查看一下 MLflow 网站文档、一些博客文章或教程视频。

照片由Adrien Converse提供,来源于Unsplash

mlflow.start_run()

这些技巧大多数是start_run()函数的参数。我们调用此函数来启动实验运行,它成为活动运行,我们可以在其中记录参数、指标和其他信息。

这是我在 MLflow 中使用最频繁的函数,它为用户提供了最即时的价值。

1. run_id

run_id是一个特定于每个实验运行的 UUID。一旦运行启动,就无法覆盖诸如模型类型或参数值等属性。然而,你可以使用run_id来回溯记录额外的值,如指标、标签或描述。

# Start MLflow run for this experiment

# End any existing runs
mlflow.end_run()

with mlflow.start_run() as run:
    # Turn autolog on to save model artifacts, requirements, etc.
    mlflow.autolog(log_models=True)

    print(run.info.run_id)

    diabetes_X = diabetes.data
    diabetes_y = diabetes.target

    # Split data into test training sets, 3:1 ratio
    (
        diabetes_X_train,
        diabetes_X_test,
        diabetes_y_train,
        diabetes_y_test,
    ) = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)

    alpha = 0.9
    solver = "cholesky"
    regr = linear_model.Ridge(alpha=alpha, solver=solver)

    regr.fit(diabetes_X_train, diabetes_y_train)

    diabetes_y_pred = regr.predict(diabetes_X_test)

    # Log desired metrics
    mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
    mlflow.log_metric(
        "rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred))
    )

在这种情况下,我们可能还希望记录此运行的决定系数(r²)值:

with mlflow.start_run(run_id="3fcf403e1566422493cd6e625693829d") as run:
    mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))

run_id可以通过从之前的运行中print(run.info.run_id)提取,或通过查询mlflow.search_runs()来获得,稍后会详细介绍。

2. experiment_id

在 MLflow 中,你可以通过几种不同的方式设置要记录的实验。第一个命令将所有后续运行的实验设置为“mlflow_sdk_test”。

mlflow.set_experiment("/mlflow_sdk_test")

这也可以通过experiment_id参数在逐次运行中进行配置。

my_experiment = mlflow.set_experiment("/mlflow_sdk_test")
experiment_id = my_experiment.experiment_id

然后,这个值可以在传递给start_run()时重复使用:

# End any existing runs
mlflow.end_run()

with mlflow.start_run(experiment_id=experiment_id):
    # Turn autolog on to save model artifacts, requirements, etc.
    mlflow.autolog(log_models=True)

    print(run.info.run_id)

    diabetes_X = diabetes.data
    diabetes_y = diabetes.target

    # Split data into test training sets, 3:1 ratio
    (
        diabetes_X_train,
        diabetes_X_test,
        diabetes_y_train,
        diabetes_y_test,
    ) = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)

    alpha = 0.8
    solver = "cholesky"
    regr = linear_model.Ridge(alpha=alpha, solver=solver)

    regr.fit(diabetes_X_train, diabetes_y_train)

    diabetes_y_pred = regr.predict(diabetes_X_test)

    # Log desired metrics
    mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
    mlflow.log_metric(
        "rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred))
    )
    mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))

3. run_name

当你指定运行名称时,你可以比依赖 MLflow 生成的默认名称更好地控制命名过程。这使你能够建立一致的实验运行命名约定,类似于你管理环境中其他资源的方式。

# Start MLflow run for this experiment

# End any existing runs
mlflow.end_run()

# Explicitly name runs
today = dt.today()

run_name = "Ridge Regression " + str(today)

with mlflow.start_run(run_name=run_name) as run:
    # Turn autolog on to save model artifacts, requirements, etc.
    mlflow.autolog(log_models=True)

    print(run.info.run_id)

    diabetes_X = diabetes.data
    diabetes_y = diabetes.target

    # Split data into test training sets, 3:1 ratio
    (
        diabetes_X_train,
        diabetes_X_test,
        diabetes_y_train,
        diabetes_y_test,
    ) = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)

    alpha = 0.5
    solver = "cholesky"
    regr = linear_model.Ridge(alpha=alpha, solver=solver)

    regr.fit(diabetes_X_train, diabetes_y_train)

    diabetes_y_pred = regr.predict(diabetes_X_test)

    # Log desired metrics
    mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
    mlflow.log_metric(
        "rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred))
    )
    mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))

但是,请注意run_name在 MLflow 中并不是唯一约束。这意味着你可能会有多个实验(具有唯一的运行 ID)共享相同的名称。

MLflow 实验表视图——重复的运行名称:作者提供的图片

这意味着每次在with语句中执行新运行时,它将创建一个同名的新实验,而不是将细节附加到此运行中。

这使我们顺利过渡到下一个参数。

4. 嵌套

如果你使用过 scikit-learn 的GridSearchCV函数来执行超参数优化,你可能对嵌套实验运行有所了解。

在 MLflow 中,嵌套实验看起来像下面这样:

MLflow 实验表视图——嵌套实验:作者提供的图片

注意,这里的指标是针对父运行保存的,它返回子运行记录的最佳值。子运行值本身是空白的。

虽然嵌套实验非常适合评估和记录参数组合以确定最佳模型,但它们也是组织工作的大型逻辑容器。通过分组实验,您可以将各个数据科学调查进行分隔,并保持实验页面的有序和整洁。

# End any existing runs
mlflow.end_run()

# Explicitly name runs
run_name = "Ridge Regression Nested"

with mlflow.start_run(run_name=run_name) as parent_run:
    print(parent_run.info.run_id)

    with mlflow.start_run(run_name="Child Run: alpha 0.1", nested=True):
        # Turn autolog on to save model artifacts, requirements, etc.
        mlflow.autolog(log_models=True)

        diabetes_X = diabetes.data
        diabetes_y = diabetes.target

        # Split data into test training sets, 3:1 ratio
        (
            diabetes_X_train,
            diabetes_X_test,
            diabetes_y_train,
            diabetes_y_test,
        ) = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)

        alpha = 0.1
        solver = "cholesky"
        regr = linear_model.Ridge(alpha=alpha, solver=solver)

        regr.fit(diabetes_X_train, diabetes_y_train)

        diabetes_y_pred = regr.predict(diabetes_X_test)

        # Log desired metrics
        mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
        mlflow.log_metric(
            "rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred))
        )
        mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))

如果需要在此嵌套运行中进行添加,则在后续执行时指定父运行的run_id作为参数,附加更多子运行。

# End any existing runs
mlflow.end_run()

with mlflow.start_run(run_id="61d34b13649c45699e7f05290935747c") as parent_run:
    print(parent_run.info.run_id)
    with mlflow.start_run(run_name="Child Run: alpha 0.2", nested=True):
        # Turn autolog on to save model artifacts, requirements, etc.
        mlflow.autolog(log_models=True)

        diabetes_X = diabetes.data
        diabetes_y = diabetes.target

        # Split data into test training sets, 3:1 ratio
        (
            diabetes_X_train,
            diabetes_X_test,
            diabetes_y_train,
            diabetes_y_test,
        ) = train_test_split(diabetes_X, diabetes_y, test_size=0.25, random_state=42)

        alpha = 0.2
        solver = "cholesky"
        regr = linear_model.Ridge(alpha=alpha, solver=solver)

        regr.fit(diabetes_X_train, diabetes_y_train)

        diabetes_y_pred = regr.predict(diabetes_X_test)

        # Log desired metrics
        mlflow.log_metric("mse", mean_squared_error(diabetes_y_test, diabetes_y_pred))
        mlflow.log_metric(
            "rmse", sqrt(mean_squared_error(diabetes_y_test, diabetes_y_pred))
        )
        mlflow.log_metric("r2", r2_score(diabetes_y_test, diabetes_y_pred))

关于这种方法要注意的一点是,您的指标现在将记录在每个运行中。

5. mlflow.search_runs()

这个技巧是使用 search_runs() 函数。

这个函数允许我们以编程方式查询实验 GUI,结果以易于理解和操作的表格格式返回。

在下面的示例中,我们可以从实验中的运行中选择特定字段,并将其加载到 Pandas DataFrame 中。请注意,可用的列远远超过实验 GUI 中提供的列!

# Create DataFrame of all runs in *current* experiment
df = mlflow.search_runs(order_by=["start_time DESC"])

# Print a list of the columns available
# print(list(df.columns))

# Create DataFrame with subset of columns
runs_df = df[
    [
        "run_id",
        "experiment_id",
        "status",
        "start_time",
        "metrics.mse",
        "tags.mlflow.source.type",
        "tags.mlflow.user",
        "tags.estimator_name",
        "tags.mlflow.rootRunId",
    ]
].copy()
runs_df.head()

由于这是一个 Pandas DataFrame,我们可以添加一些可能对分析有用的列:

# Feature engineering to create some additional columns
runs_df["start_date"] = runs_df["start_time"].dt.date
runs_df["is_nested_parent"] = runs_df[["run_id","tags.mlflow.rootRunId"]].apply(lambda x: 1 if x["run_id"] == x["tags.mlflow.rootRunId"] else 0, axis=1)
runs_df["is_nested_child"] = runs_df[["run_id","tags.mlflow.rootRunId"]].apply(lambda x: 1 if x["tags.mlflow.rootRunId"] is not None and x["run_id"] != x["tags.mlflow.rootRunId"]else 0, axis=1)
runs_df

如果我们想要汇总结果集以提供随时间变化的运行信息,我们可以使用:

pd.DataFrame(runs_df.groupby("start_date")["run_id"].count()).reset_index()

上述查询的输出:作者提供的图像

自动生成的 tags.estimator_name 字段允许我们查看每个算法的测试运行数量。

pd.DataFrame(runs_df.groupby("tags.estimator_name")["run_id"].count()).reset_index()

上述查询的输出:作者提供的图像

由于这是一个 DataFrame,我们可以将数据导出以满足任何报告要求,从而为可能没有访问工作区的用户提供所需的可见性,并在工作区之间进行比较。

结束语

这些只是如何扩展 MLflow 函数和参数在实验过程中的使用的一些示例,但在 Python API 中还有许多其他可用的函数和参数。

希望这篇文章能激发您探索一些可用的函数和参数,并查看它们是否能为您的模型开发过程带来好处。有关更多信息,请参考 API 文档,并尝试不同的配置以找到最适合您需求的配置。

如果您当前使用了我在本文中没有提到的任何函数或参数,请在评论中告诉我!

所有代码可以在我的 GitHub Repo 中找到。

作为数据专业人士展示你工作的 5 条建议

原文:towardsdatascience.com/5-recommendations-to-show-your-work-as-data-professionals-a1c8f71c9de0

对于数据专业人士而言,工作展示的建议

Chengzhi ZhaoTowards Data Science Chengzhi Zhao

·发表在Towards Data Science ·阅读时间 7 分钟·2023 年 1 月 9 日

--

照片由JIUNN-YIH LAU提供,来源于Unsplash

作为数据专业人士,你熟悉如何向领导、同事或认识的人展示你的工作。许多优秀的书籍(如《数据讲述》 作者科尔·努斯鲍默·克纳夫利克《数据故事:通过故事解释数据并激发行动》 作者南希·杜阿尔特) 涵盖了用数据讲故事的主题。完成工作只是旅程的一部分。你的工作也应该是你展示给世界的渠道。

你是否考虑过将你的工作展示给你不认识的人?比如在博客文章、YouTube、网络研讨会或会议上?

讲故事不应仅限于你熟悉的人。你可以揭示幕后故事,并更广泛地分享它们。网络上的人们会喜欢了解你如何通过用例分析、数据收集、模型构建、A/B 测试和业务影响来创造/开发一个优秀的产品/结果的故事。

互联网的信息量庞大,对于任何新数据专业人士来说,获得更广泛的受众变得更加困难。我将讨论为什么你应该首先展示你的工作,然后提供五个自我推广的技巧。

GitHub 不足以进行自我推广

许多数据专业人士认为 GitHub 账号足以让世界发现他们。今天“出色到无法忽视”并不高效于自我推广

GitHub 个人资料页面显示了你的每日贡献、你参与的仓库以及你打开的 PR。你可能还会有一些关注者和项目上的星标。这表明你在积极地从事某些工作,但你的工作并不能自我说明。它需要一个故事,一些叙事能吸引人们。

仅仅分享你的 GitHub 账号在你和世界之间设置了障碍。对你工作感兴趣的人必须在混乱的 GitHub 上自己探索,才能知道那些提交做了什么。这使得想要了解更多的读者处于一个复杂的跟随位置。这过滤掉了你的潜在追随者。

大胆地展示你的工作,并提供更多指导。直接阅读 1000 行代码而没有内容可能对一些大师有效,但我们只能假设某些人具备与你相同的熟练程度。更好的方法是展示你的过程,将你的工作编织成一个故事,并提供完整的图景。

建议 1:思考过程,分享过程,并记录过程

我们应该关注过程而不是最终结果,并使用文档来跟踪过程。

詹姆斯·克利尔(James Clear)的类似观点:

目标对设定方向有帮助,但系统最适合于取得进展。当你花太多时间思考目标而不是设计系统时,会出现一些问题。——詹姆斯·克利尔 原子习惯

尽管最终结果很重要,但它仅仅是一个方向。过程在你达到目标时占据了更多的重量。

我曾经急于查看最终结果而快速推进过程。这留下了一些粗心的漏洞,或者最终结果需要纠正。当我回顾时,如果有人问我为了达到最终决定我会采取哪些步骤。我在一些问题上迷失了方向:

  • "我们是如何得出这个事实表的粒度的?"

  • "你为什么不能应用逻辑 A,而是使用逻辑 B?"

  • "这个组件一年前的设计决策是什么?"

当我忽视文档时,这些问题的答案变得模糊,因此我的一些想法消退,变得难以追踪。

每天拥有文档能激发更多的想法。当你的工作过程被记录下来时,你的大脑会重建和连接散落的点。下一步和之前的任何错误变得清晰。

人们也乐于从你的过程中学习。如果你为开源项目做出了贡献,谈谈这个项目以及你贡献的影响,甚至提供逐步的使用说明;截取屏幕截图,制作演示视频。如果你创建了一个 APP,分享你完成的过程,并为尚未完成的项目制作演示。你分享过程并为社区提供价值,人们会关注并订阅你的邮件列表。这是完美的自然流量!

隐藏过程,只展示最终产品可能会让世界感到惊讶。然而,你的最终产品需要被发现作为一个过程,分享中间结果给了你被发现的渠道和时间,所以要注意公开展示你工作进展的力量。

推荐 2:不要害怕展示你的作品

许多人害怕展示自己的作品。我是那些被恐惧思维困扰的人之一:我在这个领域不是专家,并且对展示自己所做的事情感到过于害羞。别人会怎么想我,他们会根据我分享的内容来评判我吗?恐惧在你和读者之间设置了障碍。

你必须能够展示自己的一部分。如果人们评判你,如果他们觉得他们了解你那是你必须接受的现实——尼尔·盖曼的写作建议

你需要尝试突破害羞的心理障碍。从你最熟悉的事情开始——你的日常工作。分享一些你希望自己早知道的信息,并为开始使用数据处理库的人编写一个教程。

你需要有更强的心态来展示你的作品。不要害怕批评。这样想:很多人没有时间留下反馈。那些留下评论的人表示他们关心你的作品。无论是赞美还是批评,这都是你工作受到关注的好迹象,你应该继续下去。

推荐 3:创建一个域名来展示你的作品

你的作品需要在互联网上有一个展示的地方。社交媒体的流行度经常变化。影响者必须遵循规则以适应算法。拥有一个域名可以让你有更多的自由来制定自己的规则并为读者挑选最佳内容。如果你的社交媒体流量减少了,你的影响力和工作也会消失。现在很少有人使用 Myspace,但没有人能保证 Facebook 或 Twitter 会持续多久。拥有和维护一个域名既安全又便宜,而不是长期依赖任何社交平台。

创建一个域名和建立一个网站很简单。主要是通过像 WordPress 和 Elementor 这样的工具变得更加容易,它们允许你通过拖放来创建网站而无需编写一行代码。

当你在你的网站上展示你的工作时,你应该通过社交媒体分享它。另一种分享的选项是像 Medium 这样的博客平台,其中包括像 Toward Data Science (TDS) 这样的出版物,拥有超过 60 万关注者。你可以利用 Medium 的自定义规范链接功能同时发布你的工作。

推荐 4:专注于高质量内容,而不是货币化

你在互联网上展示的工作可以实现货币化。实现货币化需要时间和精力。当你开始时,不要考虑货币化,也不要关注关注者的数量;花更少的时间在多个社交媒体上用出色的营销图片来为你的帖子建立品牌。出版物有一些规则要遵循,通常我花不到 3 分钟就能找到一张免费的库存图片,这应该足够了。

重点是 发布高质量的内容来展示你的工作。阅读更多经典数据书籍和学习 TDS 上发布的内容将为你的思维提供源泉。高质量的内容对于长期运行至关重要,因为 SEO 在发现你的工作中起着关键作用。为出版物撰写高质量的内容可以使你的文章在 SEO 优化上走捷径。

推荐 5:教别人,从教学中学习

教学是另一种学习方式。教学和学习是双向的。当你分享你的知识时,你会发现额外的视角、反馈和想法,这些都进一步帮助你学习更多。

当我三年前写下文章 Airflow 调度间隔 101 时,我想教人们 Airflow 调度器是如何工作的。在写作过程中,我对调度器的内部核心没有很好的理解。然后我阅读了 Airflow 调度器的源代码,这帮助我获得了比 Airflow 官方文档更好的洞察。这篇文章成为了我表现最佳的文章之一,因为我提供了更多背景,获得了超过 10 万次阅读和 1 千次点赞。

Airflow 调度间隔 101

Airflow 调度间隔可能是一个具有挑战性的概念,甚至对于那些在 Airflow 上工作的开发者来说也是如此……

towardsdatascience.com

有时我会查看一些 StackOverflow 上的问题,看看是否需要知道答案。当时,我在研究并得到我的答案被接受后提供了答案。这是学习新事物并回馈的机会。

最后的想法

作为数据专业人士,你的工作不应该是秘密。我有两个例子可以分享,展示了你的工作如何为我带来更多机会。

假如你想被发现并为你的工作和自己建立品牌。在互联网上展示你的工作是你作为数据专业人士应该充分利用的绝佳方式。

我希望这个故事对你有所帮助。这篇文章是我工程与数据科学故事的系列之一,目前包含以下内容:

Chengzhi Zhao

Chengzhi Zhao

数据工程与数据科学故事

查看列表53 篇故事!

你还可以订阅我的新文章或成为推荐的 Medium 会员,获得对 Medium 上所有故事的无限访问权限。

如有疑问/评论,请随时在此故事的评论区留言或通过LinkedinTwitter直接联系我。

5 个迹象显示你的数据建模不佳

原文:towardsdatascience.com/5-signs-that-your-data-is-modeled-poorly-a646e8d33be0?source=collection_archive---------7-----------------------#2023-06-20

云时代的常见挑战

Matthew GazzanoTowards Data Science Matthew Gazzano

·

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

--

照片Jan Antonin KolarUnsplash

随着过去十年云技术的扩展和廉价存储成本的降低,许多组织积累的数据量比以前想象的要大得多。许多云数据仓库提供商提供的按需付费模式(AWSGCPAzure)减少了前期资本资源的需求和数字基础设施的考虑。

好消息是,这最终使得数据科学工作对大多数组织来说更加可及。

坏消息是数据湖正变得越来越像数据沼泽。

什么是数据建模?它面临哪些挑战?

对数据工程师来说,向高层管理人员传达一个良好建模生态系统的价值往往是困难的。这是因为利益相关者能看到的只是展示出的 BI 工具和预测模型。然而,建模不良的数据从数据治理的角度来看会给分析团队带来重大挫折。这不可避免地会拖慢工作流程,引入重复任务,降低报告准确性,以及产生许多其他负面副作用。

定义“良好建模”数据是一个独立的话题。但你可以通过以下概念在你的数据仓库中考虑它:

  • 存在一个清晰的模式来查找与业务实体相关的数据表。

  • 使用了一个有意的/已知的建模技术,例如维度模型、实体关系模型、数据保险库等。

  • 表格和字段命名约定是一致的,文档化良好,并且具有业务价值。

还应注意到,数据建模采用的是一种整体性和多系统的方法。它从你的 OLTP(在线事务处理)系统开始,这是数据最初被记录的地方。以下是一些例子:

理想情况下,你的数据在通过源系统收集时应该被规范化到第三范式。然后,它应该被摄取到分析环境中,也就是一个 OLAP(在线分析处理)系统,在这里应用了分析建模技术。在本文的背景下,OLAP 系统与云数据仓库同义。但 OLAP 系统也可以包括像 SQL Server、MySQL 或 PostgreSQL 这样的独立托管工具。

尽管数据分析师和数据科学家只与 OLAP 系统互动,但组织的数据建模策略需要考虑 OLTP 和 OLAP,才能保持可持续性。

以下是说明您的分析环境建模不佳的 5 个关键迹象。

1.) 需要部落知识来理解数据的位置

为了使新分析师在入职时取得成功,他们需要一个明确的路线图,了解数据仓库中有哪些数据,数据来源于哪里,以及其业务背景是什么。然而,数据建模不佳的团队往往难以使新员工顺利上手,因为不理解新员工回答基本业务问题为何需要如此长时间。如果没有适当的指导,分析团队可能会经历高离职率,因为新成员没有获得成功所需的工具。

数据分析师和数据科学家应该专注于解决业务问题,而不是浪费时间寻找业务实体的位置。团队越快熟悉可用数据,仪表板和预测模型的完成速度就越快。这最终会提升团队的生产力。

如果只有少数分析师知道如何回答基本的业务问题,那就是一个问题。采用这种孤立的方式无法扩展,并且只会限制团队能够解决的问题数量。

照片 by Desola Lanre-Ologun on Unsplash

2.) 不同分析师为相同指标产生不同结果

如果没有单一的真实来源,不同团队成员很容易以不同的方式计算相同的指标。例如,收入是如何定义的?计算这一指标使用了哪些表?需要有一个明确的路径来定义业务逻辑,这一切都始于一个有意图的数据模型。

我曾在这样的环境中工作,那里有 3 个不同的表表示相同的业务实体,它们使用了不同的 SQL 逻辑来得出相似但不完全相同的记录输出。再加上管理不善的报告请求队列,就会导致两个不同的分析师用不同的结果回答相同的问题。这不仅导致利益相关者对数据失去信任,还需要跨团队进行繁琐且不必要的调解工作。

3.) 团队需要重用冗余的业务逻辑代码块

我见过团队使用 Google 表格记录 SQL CASE 语句,以概述具体的业务指标。这些语句往往很长且难以阅读。虽然这种做法试图在团队之间提供一致性,但问题在于它违反了组织内的 DRY(不要重复自己)原则。

对于许多面临此类问题的团队,使用像 DBT 这样的转换工具可以让分析工程师在一个地方定义业务逻辑,然后让分析师在多个地方引用它。

想象以下例子——如果你是一个电子商务公司,且有一种复杂的方式来计算页面浏览量(这是可以的),那么为什么要在你的 BI 工具中分发和重复这个业务逻辑呢?不仅在逻辑没有被逐字复制和粘贴的情况下风险较大,而且还浪费计算资源,而这通常是大多数云服务提供商的最大开支。

为了解决这个问题,考虑绘制出需要进行的常见聚合和业务逻辑,运行一个每天(或根据需要的频率)进行的转换作业,并将其写入一个表中。然后让你的 BI 层基于这个表运行。

4.) 你的数据仓库表现不佳

如上所述,建模不良的数据会引入冗余。但它也会产生不必要的复杂性。过多的计算资源是这的副产品,并且所有云数据仓库都有一定的定价阈值限制。一旦达到这个限制,执行新的查询可能会变得极其缓慢,甚至在某些情况下不可行。

任何数据工程师都会告诉你,单纯购买额外资源并不是解决这个问题的可持续方案。

长且复杂的查询不仅执行时间长,还会消耗环境中的可用资源。考虑一个需要执行涉及 20 个连接的查询的例子。在这种情况下,理想的解决方案非常少,因为它表明用于解决业务问题的数据没有以易于访问的格式存储。这么多的连接可能会计算开销很大,尤其是当相关表的数据量很大或 ON 子句涉及多个列时。如果你正在实施一个维度模型,你的团队可能需要考虑在这些场景下在数据库中创建一个新的事实表。

资源的衡量方式因所使用的云服务提供商而异,但它们都遵循使用固定数量虚拟 CPU 的相同概念。例如,BigQuery 使用槽位的概念,槽位实际上是执行查询时可用的计算资源数量。采用按需定价的组织在任何给定时刻可使用 2000 个槽位。因此,如果一个查询非常复杂并且占用的槽位超过了可用数量,其他查询将会排队,直到它们能够被执行。

5.) 你经常需要在 SQL 中硬编码值

硬编码的值通常是数据仓库中缺失所需数据的明显迹象。在维度模型的上下文中,这通常意味着需要创建一个新的维度表来提供额外的列。

扎克·奎因 写了一篇 文章,很好地阐述了这个概念,演示了如何通过查找表消除冗长的 CASE 语句。将这个示例放在维度模型的背景下——假设你的组织需要进行大量的地理空间分析。你有一个customer_dimension表,它提供了州的缩写,但你想将其显示为完整的州名。你可以写这样的代码:

SELECT
customer_id
, customer_name
, address
, city
, state AS state_abrevaition
, CASE
    WHEN state = 'NJ' THEN 'New Jersey'
    WHEN state = 'NY' THEN 'New York'
    WHEN state = 'PA' THEN 'Pennsylvania'
    ..............
  END AS state_full_name
, zip_code
FROM customer_dimension

但这种 CASE 语句并不可持续。如果我们想更详细地改进这个解决方案,我们需要将zip_code_dimension表连接到customer_dimension表。你会看到下面的zip_code_dimension表将为我们的分析提供更大的粒度。这个表可能看起来像这样:

作者拍摄

所以有了那个新表,想象一下能够运行这个查询:

SELECT
c.customer_id
, c.customer_name
, c.address
, c.state AS state_abreviation
, z.state_name
, c.zip_code
, z.county_name
, z.country_name
, z.timezone
, z.lat
, z.lng
FROM customer_dimension c
LEFT JOIN zip_code_dimension z
  ON c.zip_code = z.zip_code

这不仅是一种更优雅和可读的查询方式来生成完整的州名,而且让我们能够回答更多的问题。通过zip_code_dimension,我们现在可以将该邮政编码的纬度和经度附加到地图可视化中,以更清晰的格式呈现。此外,还有一些其他维度字段,如果我们想在输出中包含它们(如国家、时区等),则需要数百行硬编码。

结论

如果你发现上述任何要点与你的环境相关,那么现在可能是时候全面审视你的数据管道,并了解分析团队在哪些方面需要填补空白。为了能够正确建模你的团队数据,你需要能够概念化相关的业务实体,并将其组织成有利于组织内常见问题的方式。没有一种通用的方法,但它需要对所有团队成员明确,并以一致的方式创建。

你已经成为高级 Pandas 用户的 5 个迹象

原文:towardsdatascience.com/5-signs-youve-become-an-advanced-pandas-user-without-even-realizing-it-40b81b82d369

时候该获得荣誉了

Bex T.Towards Data Science Bex T.

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

--

图片由 Barbara A Lane 提供,来自 Pixabay

介绍

你是否常常幻想 Pandas DataFrames 和 Series?你是否花费数小时进行复杂的操作和聚合,几乎没有注意到背部疼痛,一边想着“这真是太有趣了”?

你可能已经成为了一个高级 Pandas 用户而未曾意识到。加入这个少数派 Pandas 爱好者的俱乐部,接受你已经正式成为数据魔法师的事实。

那么,让我们来看看你是否属于这个俱乐部的五个迹象。

0. 了解何时抛弃 Pandas

当你刚开始学习数据分析时,Pandas 可能让你觉得它可以做所有事情。许多在线课程将 Pandas 推销为满足所有数据相关需求的一站式服务。

然而,随着经验的积累,你逐渐意识到 Pandas 有许多不足之处。你知道如何退一步问自己,“Pandas 在这里是最佳选择吗?”

有一些场景下,答案是一个大大的 NO。这些包括实时数据处理、大规模数据集处理、高性能计算和生产级数据管道。

1/ 对于实时数据处理,想象一下一个大炮,它以每小时 100 发的速度从某个过程发射实时数据片段。这些数据片段来得又快又猛,你必须在空中捕捉、处理并保存每一个。

温和地说,Pandas 会被这种级别的数据处理窒息。相反,你应该使用像 Apache Kafka 这样的库。

2/ 当涉及到大规模数据集时,Pandas 的创始人 Wes McKinney 有一个经验法则:

RAM 必须比数据集大小大 5-10 倍才能使 Pandas 达到最佳性能。

“足够简单”,如果是 2013 年你会这么说,但今天的数据集往往轻松打破这一规则。

3/ 高性能计算就像指挥一场交响乐。就像指挥需要协调许多不同音乐家的动作以创造和谐的表演一样,高性能计算任务需要协调和同步多个处理元素和线程以获得最佳结果。

至于 Pandas,它是单独运行的。

4/ 对于生产级的数据管道,可以把它们看作是供水系统。正如供水系统需要可靠、可扩展和可维护以确保稳定的清洁水源一样,数据管道也需要类似的特质。虽然 Pandas 可能负责清理和转换,但其他库应处理其余部分。

离开 Pandas 的毛茸茸的怀抱可能很困难,但如果它不够用,不要感到内疚去探索其他选项。

就个人而言,我最近对 Polars 产生了兴趣,这是一种用 Rust 编写的库,旨在从零开始解决 Pandas 的所有限制。

## 7 个简单步骤,从 Pandas 切换到闪电般快速的 Polars 并且永远不再回头

编辑描述

[towardsdatascience.com

你还可以与 datatable 这样的库进行混搭。这里是我经常用来在几分之一秒内加载大型 CSV 文件并在 Pandas 中执行分析的代码片段:

import datatable as dt

df = dt.fread("my_large_file.csv").to_pandas()

1. 速度需求

Pandas 是一个庞大的库,有许多不同的方法来执行相同的任务。然而,如果你是一个经验丰富的用户,你会知道在特定情况下哪个方法最有效。

例如,你对 applyapplymapmapiterrowsitertuples 等迭代函数之间的区别非常熟悉。你还了解在使用较慢的替代方案以获得更好的功能和使用最佳方案以获得最佳速度之间的权衡。

虽然有些人可能会称你为挑剔,但你会仔细使用 ilocloc,因为你知道 iloc 在索引行时更快,而 loc 在索引列时更快。

然而,当涉及到索引值时,你会避免使用这些访问器,因为你知道使用[query](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html)函数进行条件索引快上好几个数量级。

# DataFrame of stock prices
stocks_df = pd.DataFrame(
    columns=['date', 'company', 'price', 'volume']
)

threshold = 1e5

# Rows where the average volume for a company 
# is greater than some threshold
result = df.query(
    '(volume.groupby(company).transform("mean") > @threshold)'
)

你也知道 replace 函数是 query 的好伙伴,用于替换值。

df.query('category == "electronics"').replace(
    {"category": {"electronics": "electronics_new"}}, inplace=True
)

此外,你熟悉不同的文件格式,如 CSV、Parquets、Feathers 和 HDFs,并且会根据需要有意识地选择这些格式,而不是盲目地将所有内容倒入传统的 CSV 文件中。你知道,选择正确的格式可以帮助节省大量时间和内存资源。

除了文件格式,你还有一个强大的技巧——向量化!

与其将 DataFrames 视为普通的 数据框,你更倾向于将它们视为矩阵,将列视为向量。每当你发现自己想使用类似 applyitertuples 的迭代函数时,你首先会看看是否可以利用向量化将一个函数同时应用于一列中的所有元素,而不是逐个应用。

此外,你更喜欢使用具有 .values 属性的底层 NumPy 数组,而不是 Pandas Series,因为你亲眼见证了使用 NumPy 数组进行向量化的速度要快得多。

## 如何提高 Pandas 速度并在毫秒内处理 1000 万行数据集

编辑描述

[towardsdatascience.com

当一切都失败时,你不会就此放弃。绝不。

你会转向 CythonNumba 进行真正的计算密集型任务,因为你是专家。虽然大多数人学习了 Pandas 的基础知识,你却花费了几小时痛苦的时间来学习这两项技术。这就是你与众不同的地方。

好像这些还不够,你还认真阅读了 Pandas 用户指南的 提升性能 页面。

2. 如此多的数据类型

Pandas 提供了如此多的数据类型灵活性。你不满足于仅使用 floatintobject 数据类型,你已经将以下两张图片作为你的桌面壁纸:

来源:pbpython.com/pandas_dtypes.html BSD-3 许可协议。

来源:docs.scipy.org/doc/numpy-1.13.0/user/basics.types.html。SciPy 文档。

你刻意选择尽可能小的数据类型,因为你知道它对 RAM 很友好。你知道 int8int64 占用的内存少得多,浮点数也是如此。

你还像避瘟疫一样避免使用 object 数据类型,因为它是最糟糕的类型。

在读取数据文件之前,你会用 cat file_name.extension 查看其顶部几行,以决定你想为列使用哪些数据类型。然后,当使用 read_* 函数时,你会为每列填写 dtype 参数,而不是让 Pandas 自行决定。

你还尽可能在 就地 执行数据处理。否则,你知道 Pandas 会生成 DataFrames 和 Series 的副本,浪费你的内存。此外,你对 pd.Categoricalchunksize 等类和参数有很好的掌握。

3. Pandas 的朋友们

如果有一件事能让 Pandas 成为数据分析库的霸主,那就是它与数据生态系统其他部分的集成。

例如,现在你一定意识到如何将 Pandas 的绘图后端从 Matplotlib 更改为 Plotly、HVPlot、holoviews、Bokeh 或 Altair。

是的,Matplotlib 是 Pandas 的好朋友,但偶尔你也想要像 Plotly 或 Altair 这样互动性强的工具。

import pandas as pd
import plotly.express as px

# Set the default plotting backend to Plotly
pd.options.plotting.backend = 'plotly'

说到后端,你也注意到 Pandas 在全新 2.0.0 版本中添加了一个完全支持的 PyArrow 实现,用于其 read_* 函数来加载数据文件。

import pandas as pd

pd.read_csv(file_name, engine='pyarrow')

当只有 NumPy 后端时,存在许多限制,比如对非数值数据类型支持不足、几乎完全忽略缺失值或不支持复杂数据结构(日期、时间戳、分类数据)。

在 2.0.0 版本之前,Pandas 一直在开发内部解决方案,但这些方案不如一些重度用户的期望。使用 PyArrow 后端,数据加载速度显著提高,并带来了 Apache Arrow 用户熟悉的一系列数据类型:

import pandas as pd

pd.read_csv(file_name, engine='pyarrow', dtype_engine='pyarrow')

另一个我相信你在 JupyterLab 中经常使用的 Pandas 酷炫功能是 DataFrames 的样式设置。

由于 Jupyter 项目如此出色,Pandas 开发者在 .style 属性下添加了一些 HTML/CSS 魔法,因此你可以以一种揭示额外洞察的方式装饰普通的 DataFrames。

df.sample(20, axis=1).describe().T.style.bar(
    subset=["mean"], color="#205ff2"
).background_gradient(
    subset=["std"], cmap="Reds"
).background_gradient(
    subset=["50%"], cmap="coolwarm"
)

图片由作者提供。

4. 数据雕刻师

由于 Pandas 是一个数据分析和处理库,你是否能灵活地调整和转换数据集以适应你的目的,才是真正的高手标志。

尽管大多数在线课程提供了现成的、清理过的列格式数据,但野外的数据集有许多不同的形状和形式。例如,一种最令人讨厌的数据格式是基于行的(金融数据中非常常见):

import pandas as pd

# create example DataFrame
df = pd.DataFrame(
    {
        "Date": [
            "2022-01-01",
            "2022-01-02",
            "2022-01-01",
            "2022-01-02",
        ],
        "Country": ["USA", "USA", "Canada", "Canada"],
        "Value": [10, 15, 5, 8],
    }
)

df

图片由作者提供

你必须能够将行格式转换为更有用的格式,如以下示例,使用 [pivot](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pivot.html) 函数:

pivot_df = df.pivot(
    index="Date",
    columns="Country",
    values="Value",
)

pivot_df

你也可能需要执行这个操作的相反操作,称为 melt。

下面是一个使用[melt](https://pandas.pydata.org/docs/reference/api/pandas.melt.html)函数的 Pandas 示例,它将列数据转换为行数据格式:

df = pd.DataFrame(
    {
        "Date": ["2022-01-01", "2022-01-02", "2022-01-03"],
        "AAPL": [100.0, 101.0, 99.0],
        "GOOG": [200.0, 205.0, 195.0],
        "MSFT": [50.0, 52.0, 48.0],
    }
)

df

图片由作者提供

melted_df = pd.melt(
    df, id_vars=["Date"], var_name="Stock", value_name="Price"
)

melted_df

图片由作者提供

这些函数可能很难理解,甚至更难应用。

还有类似的函数,如 pivot_table,它创建一个数据透视表,可以对表中的每个值进行不同类型的聚合计算。

另一个函数是stack/unstack,它可以压缩/展开 DataFrame 索引。crosstab 计算两个或多个因素的交叉表,默认情况下,计算因素的频率表,也可以计算其他汇总统计数据。

然后是groupby。尽管这个函数的基本用法很简单,但其更高级的用例非常难以掌握。如果将 Pandas groupby 函数的内容制作成一个单独的库,它会比 Python 生态系统中的大多数库都要大。

# Group by a date column, use a monthly frequency 
# and find the total revenue for `category`

grouped = df.groupby(['category', pd.Grouper(key='date', freq='M')])
monthly_revenue = grouped['revenue'].sum()

精确选择适合特定情况的函数是你成为真正的数据雕刻师的标志。

认识 Pandas 中最难的函数,第一部分

编辑描述

towardsdatascience.com

阅读第二部分和第三部分来了解本节提到的函数的详细情况。

结论

虽然文章的标题可能看起来像是一种戏谑的方式来识别高级 Pandas 用户,但我的目的是为那些希望提升数据分析技能的初学者提供一些指导。

通过突显高级用户的一些独特习惯,我想揭示这个多功能库的一些鲜为人知但强大的功能。

无论你是经验丰富的数据专家还是刚刚起步,通过识别高级用户的特征并采纳他们的一些技巧和窍门,你都可以将数据分析提升到一个新的水平。

希望这篇文章为你提供了一些娱乐和灵感,激励你探索 Pandas 的深度,成为数据处理的高手。感谢你的阅读!

喜欢这篇文章以及它那奇特的写作风格吗?想象一下能访问到数十篇类似的文章,全部由一位才华横溢、迷人且机智的作者(就是我,顺便说一下 😃)编写。

仅需 4.99 美元的会员资格,你不仅可以访问我的故事,还可以获得 Medium 上来自最优秀、最聪明的头脑的知识宝藏。如果你使用我的推荐链接,你将获得我超级 nova 的感激之情和一个虚拟的击掌以支持我的工作。

[## 通过我的推荐链接加入 Medium - Bex T.

获取对我所有 ⚡优质⚡ 内容以及 Medium 上所有内容的独家访问权限,无限制地支持我的工作,给我买一杯咖啡…

ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----40b81b82d369--------------------------------)

图片由作者通过 Midjourney 提供。

你已经成为高级 Pythonista 的 5 个迹象,你可能都没意识到

原文:towardsdatascience.com/5-signs-youve-become-an-advanced-pythonista-without-even-realizing-it-2b1dd7ef57f3

时候到了,应该获得认可

Bex T.数据科学前沿 Bex T.

·发表于 数据科学前沿 ·阅读时间 8 分钟·2023 年 2 月 13 日

--

图片由 Charles Thonney 提供,来源于 Pixabay

介绍

你已经编写 Python 代码一段时间了,编写脚本并解决各种问题。你认为自己已经很不错了,是不是?好吧,大家抓紧了,因为你可能已经成为一名高级 Pythonista,却未曾意识到!

从闭包到上下文管理器,我列出了一些高级 Python 特性,这些特性会让你感叹:“我一直在用这些!”

即使这些概念对你来说是新的,你也将拥有一个出色的检查清单,将你的技能提升到新的水平。

1. 作用域

高级 Python 编程的一个关键方面是对作用域概念的深入了解。

作用域定义了 Python 解释器在程序中查找名称的顺序。Python 的作用域遵循 LEGB 规则(本地、封闭、全局和内置作用域)。根据这一规则,当你访问一个名称(它可以是变量、函数或类)时,解释器会按顺序在 本地封闭全局内置 作用域中查找。

让我们通过示例更好地理解每个级别。

示例 1 — 本地作用域

def func():
    x = 10
    print(x)

func() # 10
print(x) # Raises NameError, x is only defined within the scope of func()

在这里,x 仅在 本地func 的作用域中定义。因此,它在脚本的其他地方无法访问。

示例 2 — 封闭作用域

def outer_func():
    x = 20
    def inner_func():
        print(x)
    inner_func()

outer_func() # 20

包含作用域是局部作用域和全局作用域之间的中介作用域。在上面的示例中,xouter_func的局部作用域中。另一方面,x相对于嵌套的inner_func函数在包含作用域中。局部作用域总是具有对包含作用域的只读访问权限。

示例 3 — 全局作用域

x = 30

def func():
    print(x)

func() # 30

在这里,xfunc定义在全局作用域中,这意味着它们可以从当前脚本中的任何地方读取。

要在更小的作用域(局部和包含)中修改它们,应使用global关键字访问它们:

def func2():
    global x
    x = 40
    print(x)

func2() # 40
print(x) # 40

示例 4 — 内建作用域

内建作用域包括所有已经定义的库、类、函数和变量,这些都不需要显式的导入语句。Python 中的一些内建函数和变量的例子包括printlenrangestrintfloat等。

2. 函数闭包

对作用域的扎实掌握开启了另一个重要概念的大门——函数闭包。

默认情况下,函数执行完毕后,会返回到一个空白状态。这意味着其内存会被清除所有过去的参数。

def func(x):
    return x ** 2

func(3)
9
print(x) # NameError

上面,我们将 3 的值分配给了x,但函数在执行后忘记了它。如果我们不想让它忘记x的值怎么办?

这就是函数闭包发挥作用的地方。通过在某个内函数的包含作用域中定义变量,你可以将其存储在内函数的内存中,即使函数返回后也能保持。

这是一个简单的示例函数,用于计算其执行次数:

def counter():
    count = 0
    def inner():
        nonlocal count
        count += 1
        return count
    return inner

# Return the inner function
counter = counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
1
2
3

按照 Python 的所有规则,我们在第一次执行后应该已经丢失了counter变量。但是,由于它在内函数的闭包中,它会一直存在直到你关闭会话:

counter.__closure__[0].cell_contents
3

3. 装饰器

函数闭包有比简单计数器更重要的应用。其中之一是创建装饰器。装饰器是一个嵌套函数,你可以将其添加到其他函数中以增强或甚至修改它们的行为。

例如,下面我们正在创建一个caching装饰器,它记住了函数的每一个位置参数和关键字参数的状态。

def stateful_function(func):
    cache = {}
    def inner(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return inner

stateful_function 装饰器现在可以添加到可能会在相同参数上重复使用的计算密集型函数中。示例是以下递归斐波那契函数,它返回序列中的nth 数字:

%%time

@stateful_function
def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

fibonacci(1000)
CPU times: user 1.53 ms, sys: 88 µs, total: 1.62 ms
Wall time: 1.62 ms

[OUT]:

43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875

我们在不到一秒的时间里找到了斐波那契序列中的第 1000 个巨大的数字。下面是没有缓存装饰器的情况下,执行相同过程所需的时间:

%%time

def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

fibonacci(40)
CPU times: user 21 s, sys: 0 ns, total: 21 s
Wall time: 21 s

[OUT]:

102334155

计算第 40 个数字花费了 21 秒。如果没有缓存,计算第 1000 个数字将需要几天时间。

你可以在我另一篇文章中了解如何创建自己的装饰器(包括作用域和闭包)的详细信息:

## 实用的 Python 装饰器深入教程

编辑描述

towardsdatascience.com

4. 生成器

生成器是 Python 中强大的构造,可以高效地处理大量数据。

假设你在某个软件崩溃后有一个 10GB 的日志文件。为了找出出了什么问题,你必须在 Python 中高效地筛选它。

做这件事的最糟糕方法是像下面这样读取整个文件:

with open("logs.txt", "r") as f:
    contents = f.read()

    print(contents)

由于你逐行处理日志,你不需要读取所有的 10GB,只需一次处理其中的块即可。这就是你可以使用生成器的地方:

def read_large_file(filename):
    with open(filename) as f:
        while True:
            chunk = f.read(1024)
            if not chunk:
                break
            yield chunk # Generators are defined with `yield` instead of `return`

for chunk in read_large_file("logs.txt"):
    process(chunk)    # Process the chunk

上面,我们定义了一个生成器,它一次仅迭代日志文件的 1024 行。因此,末尾的for循环非常高效。在每次循环迭代中,内存中只有 1024 行文件内容。之前的块会被丢弃,而其余内容仅在需要时加载。

生成器的另一个特点是能够逐个生成元素,即使在循环之外,也可以使用next函数。下面,我们定义了一个极速的函数来生成斐波那契数列。

要创建生成器,你需要调用一次函数,然后在结果对象上调用next

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()

type(fib)
generator
print(next(fib)) # 0
print(next(fib)) # 1
print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3

你可以阅读下面的文章来了解更多关于生成器的信息。

[## 如何在 Python 中使用生成器和 yield - Real Python

生成器函数是 PEP 255 引入的一种特殊函数,返回一个懒惰的迭代器。这些是……

realpython.com](https://realpython.com/introduction-to-python-generators/?source=post_page-----2b1dd7ef57f3--------------------------------)

5. 上下文管理器

你一定已经使用上下文管理器很长时间了。它们允许开发者高效管理资源,如文件、数据库和网络连接。它们自动打开和关闭资源,从而使代码干净且无错误。

但是,使用上下文管理器和自己编写的上下文管理器之间有很大的区别。做得对的话,它们可以让你在原有功能的基础上抽象出很多样板代码。

一个流行的自定义上下文管理器的例子是计时器:

import time

class TimerContextManager:
    """
    Measure the time it takes to run
    a block of code.
    """
    def __enter__(self):
        self.start = time.time()

    def __exit__(self, type, value, traceback):
        end = time.time()
        print(f"The code took {end - self.start:.2f} seconds to execute.")

上面,我们定义了一个TimerContextManager类,它将作为我们未来的上下文管理器。它的__enter__方法定义了当我们使用with关键字进入上下文时发生的事情。在这种情况下,我们启动了计时器。

__exit__中,我们退出上下文,停止计时器,并报告经过的时间。

with TimerContextManager():
    # This code is timed
    time.sleep(1)
The code took 1.00 seconds to execute.

这是一个更复杂的示例,它使资源锁定,以便每次只能由一个进程使用。

import threading

lock = threading.Lock()

class LockContextManager:
    def __enter__(self):
        lock.acquire()

    def __exit__(self, type, value, traceback):
        lock.release()

with LockContextManager():
    # This code is executed with the lock acquired
    # Only one process can be inside this block at a time

# The lock is automatically released when the with block ends, even if an error occurs

如果你想更温和地了解上下文管理器,请查看我关于这个话题的文章。

## 如何在 Python 中构建自定义上下文管理器

编辑描述

[towardsdatascience.com

如果你想深入了解并学习所有相关内容,这里还有一篇出色的 RealPython 文章。

## 上下文管理器与 Python 的 with 语句 - Real Python

在本教程中,你将学习:你在编程中常遇到的一个问题是如何正确管理外部…

realpython.com

结论

就这样,各位!你有多少次说过,“我知道这一点!”?即使不是很多次,你现在也知道了要学习的东西,以变得更高级。

不要害怕学习新事物带来的不适。只要记住,伟大的力量伴随着(我不说了!)更多挑战性的 bug 需要修复。但嘿,你现在是个高手了,小小的调试对你来说算什么?

感谢阅读!

喜欢这篇文章吗?让我们面对现实,它的奇特写作风格?想象一下能访问更多类似的文章,全部由一位才华横溢、迷人、机智的作者(顺便说一下,就是我 :))。

仅需 4.99 美元的会员费,你不仅可以访问我的故事,还能获取来自 Medium 上最优秀、最聪明头脑的知识宝藏。如果你使用 我的推荐链接,你将获得我超级 nova 的感激和虚拟击掌以支持我的工作。

## 通过我的推荐链接加入 Medium — Bex T.

获取我所有⚡高级⚡内容的独家访问权,并在 Medium 上畅享无限支持我的工作,通过购买我…

ibexorigin.medium.com

使数据管理成功的 5 项技能

原文:towardsdatascience.com/5-skills-that-make-a-successful-data-manager-fe09bcc29c2?source=collection_archive---------14-----------------------#2023-01-09

在数据管理中生存并成功

理查德·费曼博士数据科学前沿 理查德·费曼博士

·

关注 发表在 数据科学前沿 ·7 分钟阅读·2023 年 1 月 9 日

--

图片由 Raj 的公路旅行 提供,来源于 Unsplash

在我的行业生涯中,我一直从事各种数据相关的角色,如数据管理、数据工程和数据分析。我考虑了哪些方面做得好,哪些方面相当具有挑战性,以及人们和他们的技能(包括我自己!)如何影响数据项目的成功。根据这些经验,我整理了以下一些重要技能,这些技能使得一个数据经理变得出色!请注意,这不是基于实证研究,我在这里不是作为研究者写的——这个列表仅仅是一些个人观察的汇总——所以如果你有其他技能,请在评论中添加 😃

#1 建模

“一图胜千言”。这在数据管理中同样适用。每个数据项目都会遇到需要绘制表示现状或捕捉利益相关者不同视角的图像的情况。因此,请将模型视为数据项目中高效沟通的关键技术!目前有很多复杂的建模工具支持成熟的建模语言。而且第二点更为重要:请应用成熟的建模语言!你能犯的最大错误之一就是试图将数据项目的每个方面都放在一个混乱的 PowerPoint 图表中。你最终会得到一个既不是好的数据模型也不是好的过程模型的怪物。相反,要意识到你的数据项目有动态方面(→ 过程模型的候选者)和静态方面(数据模型或部署模型的候选者),它们必须能够契合,但不一定要在一个图表中。已有成熟的语言(如 BPMN、UML、陈氏模型)和工具(如 Visual Paradigm、Modelio、Enterprise Architect)支持符合语法的语言应用或回环工程。模型就像是物理建筑的施工图,所以确保你的计划是成熟的,以避免数据架构中由项目人员不同解读模型而造成的倾斜地板和开墙。在物联网时代,模糊数据模型对物理世界也有影响,。它们限制了比较数据分析的可能性或使得用例扩展变得困难。

#2 连接人们

在一个组织中,数据管理者的角色是复杂的。这个人通常既不是独立实施数据库的 IT 专家,也不是实际负责数据或流程的业务专家(这更属于数据管家(Data Steward)的职责)。那么,数据管理者(甚至数据管理部门)的真正价值是什么呢?在我看来,你需要一个在不同的数据利益相关者之间搭建桥梁的人,从方法论的层面进行连接。很容易找到那些自认为在特定业务领域、数据分析方法或 IT 工具方面是专家的人,但找到一个愿意将这些人连接起来并组织他们的能力的人则相当复杂,因为这在数据项目中往往是必需的。因此,我所指的是像网络建设、项目管理、利益相关者管理和变更管理这样的技能,这些技能是一步一步建立数据社区作为数据治理的支柱所必需的。没有这些人,数据管理者将失败!因此,在我看来,招聘人员在寻找数据管理者时,不仅应考察技术技能,还应考察这些人际技能。同样,如果你对数据管理者职位感兴趣,评估一下自己是否对超越技术话题的各种组织职责感到舒适。

#3 领域专长

我见过各种建立数据管理组织的方法。有的团队是更大业务部门的一部分,全球负责数据标准,或者是由小核心团队和其他部门的“虚线”数据管理者组成的联邦组织,或者数据管理团队是 IT 部门的一部分。不论组织的归属在哪里——我的经验表明,一个好的数据经理应具备基本的领域知识。如果一个人作为某个业务领域的数据经理,她/他应该了解价值链中的情况,关键业务问题是什么,存在哪些数据用于何种目的,哪些业务流程生成和消耗数据,它们如何相互连接。有了这些知识,数据经理在收到数据变更请求时,能总是多想一步。也许管道中还有其他请求可以合并,也许你对流程实施有自己的想法,也许你建议涉及其他利益相关者(→见 #2),也许有必须实施的政策(例如 DSGVO),也许你可以影响数据模型的可持续性……列表很长!假设数据经理最初有 IT 背景,创建令人满意的领域知识可能是一项具有挑战性的任务,因为它是行业、公司或甚至部门特定的,你不能仅仅通过听有声书来弥补这个差距。然而,保持冷静,并接受随着时间的推移会有学习曲线,但重要的是你愿意学习和吸收这些非 IT 知识。

#4 技术意识

将数据变更请求从业务相关方的期望转化为真正需要实施的内容是一个过程。在我所在的“家”行业——半导体行业中,业务专家通常也拥有先进的 IT 知识——这既是福也是祸 😃 一方面,你可以与所有相关方深入探讨技术问题;另一方面,他们倾向于提出 IT 解决方案建议而不是问题描述,这在 IT 专家中并不受欢迎,因为这在某种程度上绕过了他们的责任。除了受损的荣誉感之外,还有充分的理由解释为什么构建实施概念与需求分析是分开的。其中之一是没有“唯一”的解决方案,而是各种优缺点的选项。领域专长的重要性与对最新数据管理技术及其能力的意识同样重要。例如,并不是每一条主数据都必须手动输入,即使数据管理员期望如此。也许记录中的某些属性可以通过业务规则系统推导出来,或者通过其他系统的数据集成进行填写。或者,问题可以通过更改业务流程中的任务顺序来解决,这可以通过工作流得到支持和强制执行。此外,数据目录还有一些强大的功能,帮助数据管理者在其全面角色中——请查看我关于某个工具的系列文章以获取更多信息。特别是对于那些没有 IT 背景的数据管理者,这项技能可能具有挑战性。幸运的是,技术知识通常不是公司特有的,因此可以通过官方材料(视频、MOOC、书籍、文章等)来弥补。从教学的角度来看,我建议为初级数据管理者制定一个学习路径,以便概述和深入了解所选技术。

#5 精益思维

最后但同样重要的是,我在应用精益哲学方面有过良好的经验。“精益” 通常与制造业相关,但也可以用于数据管理。例如,你可以利用 “Poka Yoke” 原则来设计数据流程或图形用户界面,以通过设计避免某些数据质量问题。或者,你可以应用 “5S” 来清理数据架构,其中数据评估有助于判断你组织中的哪些数据对象是“浪费”,哪些是业务关键的 —— 我们已发表了一篇提出这种方法的研究论文。尤其是在较大的数据项目中,例如当你想将整个领域整合到主数据管理系统中时,组织 “Kaizen” 事件是值得的,在这种活动中,所有利益相关者以研讨会模式共同工作,塑造未来(概念上的)数据景观。另一个很好的方法是 “PDCA” ,它有助于迭代地改善数据质量 —— 还有更多精益方法我已经在数据项目中应用过且非常有用!在我看来,精益方法是获得可持续数据架构的关键。这种可持续性效应也可以在传统的精益管理应用中观察到,如在 这篇(德语)文章 中讨论的那样。价值链中的通用浪费类型也可以在数据架构中找到,例如运动(例如,数据维护的不必要的人工努力)和缺陷(例如,错误或缺失的数据限制了数据产品的有效性)。总的来说,这是一项可能被低估的技能,但却是进一步研究的绝佳主题!

结论

通过这个故事,我想分享我对数据经理最佳技能组合的看法。一个好的数据经理远不止是数据管理软件的技术专家。通常,数据管理计划的成功(或失败)取决于组织中的(缺失的)支持。因此,有一些非技术技能,如利益相关者管理或项目管理,特别是在更复杂的数据项目中,需要学习和应用。尽管如此,IT 知识也很重要,尤其是在通过精益方法发现数据架构中的浪费时。领域知识是你在大学里学不到的,而是通过在某一业务领域的实际工作逐渐获得的,但你也应该对这种知识保持开放,并且可以通过采访专家等方式积极影响你的学习曲线的坡度!别忘了在你的数据项目中应用已建立的建模技术!你组织中的人们越习惯于良好的模型,未来的数据项目沟通将变得越高效。

使用 Python 构建美丽条形图的 5 个步骤

原文:towardsdatascience.com/5-steps-to-build-beautiful-bar-charts-with-python-3691d434117a

如何利用 Matplotlib 的全部功能讲述更具吸引力的故事

Guillaume WeingertnerTowards Data Science Guillaume Weingertner

·发表在 Towards Data Science ·7 分钟阅读·2023 年 1 月 23 日

--

2008 年美国国内航班的平均航空延误 — 作者提供的图像

动机

当支持这些故事的图表清晰自解释视觉上令人愉悦时,讲述一个引人入胜的故事变得容易得多。

在许多情况下,内容和形式同样重要。

数据再好但表现不佳将不会引起应有的关注,而表现出色的数据即使质量较差也容易被忽视。

Matplotlib 使得使用现成函数绘制数据变得快捷而简单,但微调步骤则需要更多的努力。

我花了不少时间研究使用 Matplotlib 构建引人注目的图表的最佳实践,以便你不必花费时间。

在本文中,我重点介绍了条形图,并解释了如何将我在各处找到的知识片段拼接在一起,从而达到如下效果…

2008 年美国国内航班的平均航空延误 — 作者提供的图像

… 到那个:

2008 年美国国内航班的平均航空延误 — 作者提供的图像

#0 数据

为了说明方法论,我使用了一个关于 2008 年美国国内航班延误的公开数据集:

2008 年,“数据展览 2009:航空公司准时数据”, doi.org/10.7910/DVN/HG7NV7,哈佛数据集,V1

公有领域 CC0 1.0

在导入了读取数据和构建图表所需的包后,我简单地按月份对数据进行了分组,并计算了平均延迟,使用了以下代码:

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.ticker import MaxNLocator

df = pd.read_csv('DelayedFlights.csv')

df = df[['Month', 'ArrDelay']] # Let's only keep the columns useful to us
df = df[~df['ArrDelay'].isnull()] # Get rid of cancelled and diverted flights

# Group by Month and get the mean
delay_by_month = df.groupby(['Month']).mean()['ArrDelay'].reset_index() 

本文中用于构建不同版本条形图的数据集如下:

本文中使用的数据集 —— 图片由作者提供

#1 基础图表

坦白说,使用两行代码,你已经可以构建一个条形图并从中获得一些基本的见解。

虽然这个图表既不最美观,也不最实用,因为缺少关键信息,但你已经可以看出,12 月份旅行很可能会导致航班延误。

# Create the figure and axes objects, specify the size and the dots per inches 
fig, ax = plt.subplots(figsize=(13.33,7.5), dpi = 96)

# Plot bars
bar1 = ax.bar(delay_by_month['Month'], delay_by_month['ArrDelay'], width=0.6)

Matplotlib 最基本的条形图 —— 图片由作者提供

#2 基本要素

让我们在图表中添加一些重要元素,以使其更易于观众阅读。

  • 网格

    为了提高可读性,图表的网格是必不可少的。它们的透明度设置为 0.5,以免过多干扰数据点。

  • X 轴和 Y 轴重新格式化

    我主动添加了比实际需要更多的参数,以获得对微调可能性的更全面的视角。例如,x 轴不需要 major_formatter 和 major_locator 对象,因为我们只是设置标签,但如果读者的 x 轴包含其他数字,那么这些可能会派上用场。

  • 条形标签

    在每个条形上方添加条形标签,使得对接近数据点之间的比较更容易,并提供关于实际值的更多细节。

# Create the grid 
ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1)
ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1)

# Reformat x-axis label and tick labels
ax.set_xlabel('', fontsize=12, labelpad=10) # No need for an axis label
ax.xaxis.set_label_position("bottom")
ax.xaxis.set_major_formatter(lambda s, i : f'{s:,.0f}')
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.xaxis.set_tick_params(pad=2, labelbottom=True, bottom=True, labelsize=12, labelrotation=0)
labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
ax.set_xticks(delay_by_month['Month'], labels) # Map integers numbers from the series to labels list

# Reformat y-axis
ax.set_ylabel('Delay (minutes)', fontsize=12, labelpad=10)
ax.yaxis.set_label_position("left")
ax.yaxis.set_major_formatter(lambda s, i : f'{s:,.0f}')
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
ax.yaxis.set_tick_params(pad=2, labeltop=False, labelbottom=True, bottom=False, labelsize=12)

# Add label on top of each bar
ax.bar_label(bar1, labels=[f'{e:,.1f}' for e in delay_by_month['ArrDelay']], padding=3, color='black', fontsize=8) 

在我们的图表中添加一些基本功能 —— 图片由作者提供

#3 专业外观

在我们的图表中添加更多功能会使其看起来更加专业。这些功能可以叠加在任何图表上(不仅仅是条形图),并且与我们在本文中使用的数据无关。

感谢下面的代码片段,这些调整将几乎不需要任何额外的努力来实现。作者的建议:保存并随意重用。

读者可以调整这些参数来创建自己的视觉身份。

  • 坐标轴

    坐标轴构成了图表周围可见的框架。它们被移除,除了右侧的坐标轴被设置得稍微粗一点。

  • 顶部的红线和矩形

    在标题上方添加了一条红线和一个矩形,以很好地将图表与上方的文本隔离开来。

  • 标题和副标题

    一个没有标题来介绍它的图表算什么呢?

    副标题可以用来进一步解释内容,甚至提出初步结论。

  • 来源 在所有生产的图表中必不可少。

  • 边距调整

    调整图表区域周围的边距,以确保充分利用所有可用空间。

  • 白色背景

    设置白色背景(默认透明)在通过电子邮件、Teams 或其他工具发送图表时会非常有用,因为透明背景可能会造成问题。

# Remove the spines
ax.spines[['top','left','bottom']].set_visible(False)

# Make the left spine thicker
ax.spines['right'].set_linewidth(1.1)

# Add in red line and rectangle on top
ax.plot([0.12, .9], [.98, .98], transform=fig.transFigure, clip_on=False, color='#E3120B', linewidth=.6)
ax.add_patch(plt.Rectangle((0.12,.98), 0.04, -0.02, facecolor='#E3120B', transform=fig.transFigure, clip_on=False, linewidth = 0))

# Add in title and subtitle
ax.text(x=0.12, y=.93, s="Average Airlines Delay per Month in 2008", transform=fig.transFigure, ha='left', fontsize=14, weight='bold', alpha=.8)
ax.text(x=0.12, y=.90, s="Difference in minutes between scheduled and actual arrival time averaged over each month", transform=fig.transFigure, ha='left', fontsize=12, alpha=.8)

# Set source text
ax.text(x=0.1, y=0.12, s="Source: Kaggle - Airlines Delay - https://www.kaggle.com/datasets/giovamata/airlinedelaycauses", transform=fig.transFigure, ha='left', fontsize=10, alpha=.7)

# Adjust the margins around the plot area
plt.subplots_adjust(left=None, bottom=0.2, right=None, top=0.85, wspace=None, hspace=None)

# Set a white background
fig.patch.set_facecolor('white')

我们的视觉身份应用到图表上,使其更整洁 —— 图片由作者提供

#4 色彩渐变

我们在上一节中留下的图表干净整洁,准备好被纳入演示中。虽然对条形颜色进行渐变以更好地可视化变化并非必需,但会增加一个吸引人的特性。

这个用例的在线文档可能不是最好的,但实际上用 Matplotlib 的LinearSegmentedColormapNormalize函数实现起来并不太难。

# Colours - Choose the extreme colours of the colour map
colours = ["#2196f3", "#bbdefb"]

# Colormap - Build the colour maps
cmap = mpl.colors.LinearSegmentedColormap.from_list("colour_map", colours, N=256)
norm = mpl.colors.Normalize(delay_by_month['ArrDelay'].min(), delay_by_month['ArrDelay'].max()) # linearly normalizes data into the [0.0, 1.0] interval

# Plot bars
bar1 = ax.bar(delay_by_month['Month'], delay_by_month['ArrDelay'], color=cmap(norm(delay_by_month['ArrDelay'])), width=0.6, zorder=2)

仍然是相同的条形图,增加了额外的颜色特性——图片由作者提供

#5 最后修饰

为了实现文章开头介绍的最终结果,剩下的工作就是实现这几个额外的组件:

  • 平均数据线

    在图表上显示平均数据线是帮助观众快速了解情况的一种有效方式。

  • 第二种颜色刻度

    通过第二种颜色刻度,我们突出了高于平均水平(或任何阈值)的数据,以便在短时间内更容易理解可视化内容。

  • 图例

    当我们添加第二种颜色刻度时,我们在图表上引入了图例的需求。

# Find the average data point and split the series in 2
average = delay_by_month['ArrDelay'].mean()
below_average = delay_by_month[delay_by_month['ArrDelay']<average]
above_average = delay_by_month[delay_by_month['ArrDelay']>=average]
# Colours - Choose the extreme colours of the colour map
colors_high = ["#ff5a5f", "#c81d25"] # Extreme colours of the high scale
colors_low = ["#2196f3","#bbdefb"] # Extreme colours of the low scale

# Colormap - Build the colour maps
cmap_low = mpl.colors.LinearSegmentedColormap.from_list("low_map", colors_low, N=256)
cmap_high = mpl.colors.LinearSegmentedColormap.from_list("high_map", colors_high, N=256)
norm_low = mpl.colors.Normalize(below_average['ArrDelay'].min(), average) # linearly normalizes data into the [0.0, 1.0] interval
norm_high = mpl.colors.Normalize(average, above_average['ArrDelay'].max())

# Plot bars and average (horizontal) line
bar1 = ax.bar(below_average['Month'], below_average['ArrDelay'], color=cmap_low(norm_low(below_average['ArrDelay'])), width=0.6, label='Below Average', zorder=2)
bar2 = ax.bar(above_average['Month'], above_average['ArrDelay'], color=cmap_high(norm_high(above_average['ArrDelay'])), width=0.6, label='Above Average', zorder=2)
plt.axhline(y=average, color = 'grey', linewidth=3)

# Determine the y-limits of the plot
ymin, ymax = ax.get_ylim()
# Calculate a suitable y position for the text label
y_pos = average/ymax + 0.03
# Annotate the average line
ax.text(0.88, y_pos, f'Average = {average:.1f}', ha='right', va='center', transform=ax.transAxes, size=8, zorder=3)

# Add legend
ax.legend(loc="best", ncol=2, bbox_to_anchor=[1, 1.07], borderaxespad=0, frameon=False, fontsize=8)

最终产品:图表易于阅读——图片由作者提供

#6 最终思考

本文的目的是分享这里和那里的知识,以使用 Matplotlib 构建更具吸引力的条形图。我尽量使其尽可能实用,包含可重用的代码片段。

我相信还有其他调整是我没想到的。如果你有任何改进的建议,欢迎评论,让这篇文章对大家更有用!

本文仅关注于条形图,敬请期待更多内容!

感谢你阅读到文章的最后!

如果你有任何问题或意见,欢迎在下方留言,或者通过* LinkedIn 与我联系!

5 步骤用 Python 绘制美观的折线图

原文:towardsdatascience.com/5-steps-to-build-beautiful-line-charts-with-python-655ac5477310

如何使用 Matplotlib 的全部功能来讲述更具吸引力的故事

Guillaume WeingertnerTowards Data Science Guillaume Weingertner

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 10 月 27 日

--

5 个最富有国家的 GDP 随时间的演变 — 图片由作者提供

动机

几个月前,我写了一篇关于条形图的文章,讨论了如何使它们清晰自我解释视觉上令人愉悦,以讲述更具吸引力的故事(链接见下文)。

## 5 步骤用 Python 绘制美观的条形图

如何使用 Matplotlib 的全部功能来讲述更具吸引力的故事

towardsdatascience.com

在这篇文章中,我转而探讨折线图,它们具有其他值得探索的特性。

Matplotlib 使得使用现成函数绘制数据变得快捷而简单,但细节调整步骤则需要更多的努力。

我花了相当多的时间研究使用 Matplotlib 构建引人入胜的图表的最佳实践,因此你不必亲自去做。

目的是从这个…

… 变成这样:

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

#0 数据

为了说明方法,我使用了一个包含过去 50 年各国 GDP 信息的公开数据集:

来源:世界银行国家账户数据和 OECD 国家账户数据文件。

许可网址:datacatalog.worldbank.org/public-licenses#cc-by

许可类型:CC BY-4.0

在导入了读取数据和构建图表所需的包之后,我简单地筛选了 2022 年的前 20 个国家:

import pandas as pd
import matplotlib.pyplot as plt
from datetime import timedelta

# Read the data
df = pd.read_csv('88a1e584-0a94-4e73-b650-749332831ef4_Data.csv', sep=',')
df.drop(['Series Name', 'Series Code', 'Country Code'], axis=1, inplace=True)
df = df.dropna(subset=['Country Name'])

# Filter on the Top 20 richest countries of 2022
top_20_countries = df[df['Year'] == '2022-01-01'].sort_values('GDP', ascending = False).head(20)['Country Name'].tolist()
df = df[df['Country Name'].isin(top_20_countries)].reset_index(drop = True)

df.head()

文章中用于构建不同版本折线图的数据集如下:

本文使用的数据集摘录 — 作者提供的图片

#1 基本图

首先,4 行代码足以创建图形并循环绘制各国的折线:

# Create the figure and axes objects, specify the size and the dots per inches 
fig, ax = plt.subplots(figsize=(13.33,7.5), dpi = 96)

# Plot lines
for country in top_20_countries:
    data = df[df['Country Name'] == country]
    line = ax.plot(data['Year'], data['GDP'], label=country)

Matplotlib 中最基本的折线图 — 作者提供的图片

#2 必要功能

让我们在图表中添加一些重要元素,以使其对观众更具可读性。

  • 网格

    为了提高可读性,图表的网格至关重要。它们的透明度设置为 0.5,以免对数据点产生过多干扰。

  • X 轴和 Y 轴格式调整

    我故意添加了比必要更多的参数,以便更全面地查看微调的可能性。例如,x 轴不需要 major_formatter 和 major_locator 对象,因为我们只显示年份,但如果读者的 x 轴包含其他数据,这将非常有用。

  • 图例

    由于我们展示了许多线条,因此重要的是添加标签和图例,以便区分各条线。

# Add legend
ax.legend(loc="best", fontsize=8)

# Create the grid 
ax.grid(which="major", axis='x', color='#DAD8D7', alpha=0.5, zorder=1)
ax.grid(which="major", axis='y', color='#DAD8D7', alpha=0.5, zorder=1)

# Reformat x-axis label and tick labels
ax.set_xlabel('', fontsize=12, labelpad=10) # No need for an axis label
ax.xaxis.set_label_position("bottom")
#ax.xaxis.set_major_formatter(lambda s, i : f'{s:,.0f}') #in case we need additional formatting
#ax.xaxis.set_major_locator(MaxNLocator(integer=True)) #in case we need additional formatting
ax.xaxis.set_tick_params(pad=2, labelbottom=True, bottom=True, labelsize=12, labelrotation=0)

# Reformat y-axis
ax.set_ylabel('GDP (Billions USD)', fontsize=12, labelpad=10)
ax.yaxis.set_label_position("left")
ax.yaxis.set_major_formatter(lambda s, i : f'{s*10**-9:,.0f}')
#ax.yaxis.set_major_locator(MaxNLocator(integer=True)) #in case we need additional formatting
ax.yaxis.set_tick_params(pad=2, labeltop=False, labelbottom=True, bottom=False, labelsize=12)

在我们的图表中添加一些必要功能 — 作者提供的图片

#3 专注于讲述故事

现在是时候突出显示图表中需要突出的内容,以讲述其背后的故事。在这种情况下,我们将突出显示 5 个最富有的国家,并跟踪它们随时间变化的 GDP。

我们在字典中定义了具体的颜色和线条样式,并稍微修改了代码以单独绘制它们。

# Color and line style
colors_dict = {'United States': '#014f86', 'China': '#DC0000', 'Japan': '#ff4d6d', 'Germany': '#403d39', 'India': '#6a994e'}
line_styles_dict = {'United States': '-', 'China': '-', 'Japan': '-', 'Germany': '-', 'India': '-'}

# Plot the Top 5 lines
for country in top_20_countries[:5]:
    color = colors_dict.get(country, 'grey')  # get the color from the dictionary, default to grey if not found
    line_style = line_styles_dict.get(country, '-')  # get the line style from the dictionary, default to solid line if not found
    data = df[df['Country Name'] == country]
    line = ax.plot(data['Year'], data['GDP'], color=color, linestyle=line_style, zorder=2, label=country)

# Add legend
ax.legend(loc="best", fontsize=8)

# Plot the rest
for country in top_20_countries[5:]:
    data = df[df['Country Name'] == country]
    line = ax.plot(data['Year'], data['GDP'], color='grey', linestyle=':', linewidth=0.5, zorder=2)

仍然是相同的折线图,但故事更加清晰 — 作者提供的图片

#4 专业外观

在图表中添加更多功能将使其看起来更专业。这些功能会叠加在任何图表之上,并且与我们在本文中使用的数据无关。

借助以下代码片段,这些调整将几乎无需努力即可实现。作者建议:保存并随意重用。

读者可以调整这些元素以创建自己的视觉风格。

  • 坐标轴

    坐标轴组成了图表周围可见的框架。它们被移除,除了左侧的坐标轴,它被设置为稍微粗一点。

  • 红线和矩形

    在标题上方添加了红线和矩形,以便将图表与上面的文本清晰隔离开来。

  • 标题和副标题

    没有标题的图表如何引人注目呢?

    副标题可用于进一步解释内容或甚至提出初步结论。

  • 来源 所有生产的图表中必不可少。

  • 边距调整

    调整了围绕图表区域的边距,以确保充分利用所有可用空间。

  • 白色背景

    设置白色背景(默认为透明)在通过电子邮件、Teams 或其他工具发送图表时会很有用,因为透明背景可能会带来问题。

# Remove the spines
ax.spines[['top','right','bottom']].set_visible(False)

# Make the left spine thicker
ax.spines['left'].set_linewidth(1.1)

# Add in red line and rectangle on top
ax.plot([0.05, .9], [.98, .98], transform=fig.transFigure, clip_on=False, color='#E3120B', linewidth=.6)
ax.add_patch(plt.Rectangle((0.05,.98), 0.04, -0.02, facecolor='#E3120B', transform=fig.transFigure, clip_on=False, linewidth = 0))

# Add in title and subtitle
ax.text(x=0.05, y=.93, s="Evolution of the 20 Richest Countries GDP over the Past 50 Years", transform=fig.transFigure, ha='left', fontsize=14, weight='bold', alpha=.8)
ax.text(x=0.05, y=.90, s="Focus on the current 5 richest countries from 1973 to 2022", transform=fig.transFigure, ha='left', fontsize=12, alpha=.8)

# Set source text
ax.text(x=0.05, y=0.12, s="Source: World Bank - https://databank.worldbank.org/", transform=fig.transFigure, ha='left', fontsize=10, alpha=.7)

# Adjust the margins around the plot area
plt.subplots_adjust(left=None, bottom=0.2, right=None, top=0.85, wspace=None, hspace=None)

# Set a white background
fig.patch.set_facecolor('white')

我们的视觉识别应用于图表,使其更加整洁 —— 作者提供的图片

#5 最后的修饰

为了达到文章开头介绍的最终结果,剩下的工作就是实现这些额外的组件:

  • 终点标记

    这些元素纯粹是美学上的,但为我们的折线图增添了亮点。我们用标记突出显示每条线的最后一个点,使其更为显眼。

  • 注释

    多亏了注释方法,我们可以在图表中突出显示特定点,并直接添加评论。

# Plot the Top 5 lines
for country in top_20_countries[:5]:
    color = colors_dict.get(country, 'grey')  # get the color from the dictionary, default to black if not found
    line_style = line_styles_dict.get(country, '-')  # get the line style from the dictionary, default to solid line if not found
    data = df[df['Country Name'] == country]
    line = ax.plot(data['Year'], data['GDP'], color=color, linestyle=line_style, zorder=2, label = country)
    ax.plot(data['Year'].iloc[-1], data['GDP'].iloc[-1], 'o', color=color, markersize=10, alpha=0.3)
    ax.plot(data['Year'].iloc[-1], data['GDP'].iloc[-1], 'o', color=color, markersize=5)

# Add some text on the graph
ax.annotate('During the 2000s,\nChina began experiencing rapid economic growth,\noutpacing all other countries.',
            (data['Year'].iloc[-18], 2000000000000),
            xytext=(data['Year'].iloc[-28]-timedelta(days=500), 18000000000000),
            ha='left', fontsize=9, arrowprops=dict(arrowstyle='-|>', facecolor='k', connectionstyle="arc3,rad=-0.15"))

最终产品:折线图易于阅读 —— 作者提供的图片

#6 最后的思考

这篇文章的目的是分享在制作更具吸引力的折线图时积累的知识。我尽量使其尽可能实用,提供了可重复使用的代码片段。

我相信还有其他调整是我没想到的。如果你有任何改进建议,请随时评论,让这篇文章对大家更有帮助!

本文仅专注于折线图,敬请关注更多内容!

感谢你阅读到文章的最后。 关注以获取更多内容! 如果有任何问题或建议,请随时在下方留言,或通过 LinkedIn / X 联系我!

[## 订阅以获取 Guillaume Weingertner 发布的新邮件。

订阅以获取 Guillaume Weingertner 发布的新邮件。通过注册,你将创建一个 Medium 账户(如果你还没有的话)…

guillaume-weingertner.medium.com](https://guillaume-weingertner.medium.com/subscribe?source=post_page-----655ac5477310--------------------------------)

用 Python 展示地理空间数据同时变化的 5 种可视化方法

原文:towardsdatascience.com/5-visualizations-with-python-to-show-simultaneous-changes-in-geospatial-data-ddc2eaab9d78

表达多个位置随时间变化的数据值的想法

Boriharn KTowards Data Science Boriharn K

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

--

John Matychuk的照片,来源于Unsplash

时间和空间在一些科幻电影中被设定为主要主题,比如《星际穿越》,这是我最喜欢的电影。这类电影有趣的原因之一是它们讲述了发生在不同位置的同时故事,并且这些故事彼此影响。

地理空间数据是一种包含空间信息和属性的数据,这些属性是事件或对象的特征。

如果将数据可视化视为讲述故事的一种方式,地理空间数据随着时间的属性变化可以成为创建有趣情节的良好素材,因为它与科幻电影共享一些概念。

本文中使用 Python 构建的图表示例,以表达地理空间数据的同时变化。图片由作者提供。

本文将指导你使用 Python 代码中的图表和技术来表达地理空间数据的同时变化。

让我们开始吧!!

获取数据

几何数据

从导入库开始。我们主要使用GeoPandas,这是一个有用且易于使用的地理空间数据绘图库。

import numpy as np
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

接下来,可以从geojson.xyz获取地理空间信息,这是一 个开源网站,提供 GeoJSON 数据用于网页地图示例和实验。

我们将使用的是美国西部州的边界几何数据。下面的代码展示了如何获取 几何数据,不包括夏威夷和阿拉斯加。

本区域将是本文的主要区域。如果您想选择不同的州,请随意修改下面的列表。

既然我们已有几何图形,让我们绘制地理空间数据。

可以使用 contextily 库将基础地图作为背景添加,使图表看起来更真实。方法见下面的代码。

使用 GeoPandas 绘制地理空间数据。图像由作者提供。

数据值

为了展示本文中的方法如何应用于实际数据,将使用 Wikipedia 上的“美国各州和领地按收入分类”信息作为示例,以表达数据值的年度变化。

数据在 CC BY-SA 4.0 国际许可证 下使用。

只需几行代码,信息即可通过 Panda 的 read_html 函数下载。请注意,检索到的表格没有来自 2020 年的信息。

接下来,将获得的 DataFrame 与之前的 DataFrame 合并。

df_m = pd.merge(df, df_w, on='name')
df_m.head()

中位家庭收入值必须转换为整数,因为获取的数据值是字符串格式。顺便说一下,字符串格式的值用于标注输出图表。

可视化

本文将提供 5 个图表创意,用于展示地理空间数据的同步变化。

让我们开始吧!!

1. 使用空间区域和颜色进行地理分层图

地理分层图是一种典型的空间图表,根据地理特征显示颜色。通过结合多个图表,我们可以表达时间上的变化。

从下面的代码开始,循环函数将用于按年份绘制地理分层图。除了颜色映射外,标注数据值可以帮助展示中位收入值的进展。

请注意,获得的图表将保存在您的计算机上,以便在下一步导入。

一个地理分层图的示例。图像由作者提供。

现在我们已有多个图表,以下方法是将它们组合在一起的创意。

1.1 创建照片拼贴

第一个想法是将所有图表结合在一起作为照片拼贴。这可以被认为是制作信息图的一个好选择。

让我们定义一个函数来完成任务。我在这个链接上找到了一个有用的代码和方法来创建照片拼贴。

接下来,将图表导入并与定义的函数结合。

瞧!!

结合分级地图的照片拼贴的一部分。图片由作者提供。

照片拼贴可以展示并帮助比较地理空间数据的同时变化。从结果来看,可以很容易地区分加利福尼亚州的中位数家庭收入最高,而新墨西哥州的收入最低。

1.2 创建动画

除了将地图并排放置,我们还可以将它们收集在一起以创建动画。如果你使用的媒体允许以 GIF 文件格式显示动态图像,这可能是一个不错的选择来吸引读者的注意。

哒哒!!

显示地理空间数据同时变化的分级地图动画。图片由作者提供。

2. 使用气泡图比较大小

与之前的方法相同,气泡图中的圆形区域可以用来表示数据值。这些气泡位于州的重心位置,大小会随着年度数据值的变化而变化。

为了后续的颜色映射,我们将提取一个颜色调色板以创建颜色列表。

接下来,使用 Python 的 for-loop 函数绘制每年的气泡图。

空间图上的气泡图示例。图片由作者提供。

创建气泡图后,我们将它们组合起来以表达空间数据的变化。

2.1 创建照片拼贴

使用之前定义的函数来获取照片拼贴。

get_collage(3, 4, 900*3, 650*4,
            ['bubble_'+y+'.png' for y in y_list], 'bubble_collage.png') 

一部分结合了气泡图的照片拼贴。图片由作者提供。

2.2 创建动画

将图表组合在一起,创建一个动态图 GIF 文件。

img = [Image.open('bubble_'+y+'.png') for y in y_list]
imageio.mimsave('bubble.gif', img, duration=0.4)

显示地理空间数据同时变化的气泡图动画。图片由作者提供。

与分级地图相比,除了标记数据和用颜色映射外,圆形区域也有助于比较空间数据值。我们可以看到圆圈的大小会随时间更新。

3. 使用饼图显示比例

如果目标是局部比较数据值,可以使用饼图来方便阅读,因为它们能够显示每个位置与其他位置的比例。

为了让每个位置的数据更加突出,诀窍是将每个位置的片段与其他部分分开。这将有助于更容易地区分比例。可以在下面的代码中看到设置的‘explode’参数。

空间图上的饼图示例。图像由作者提供。

创建照片拼贴

让我们结合结果。

get_collage(3, 4, 980*3, 750*4,
            ['pie_'+ y +'.png' for y in y_list],
            'pie_collage.png')

一个结合饼图的照片拼贴部分。图像由作者提供。

创建动画也可以使用之前提到的方法,如区域图和气泡图所示。

4. 使用柱状图简化事物

到目前为止,之前提到的技术都是将多个图表结合起来。它们需要作为照片拼贴或动画组合在一起。当需要一个简单的单一图表时,这可能是一个限制。

没问题!!

一个简单的单一图表也可以满足需求。通过应用柱状图,所有内容可以绘制在一个图表中。主要概念是每个位置都有自己的柱状图,每根柱子代表每年的值。颜色映射将用于帮助比较柱子的数值。

这种方法可以作为本文中区域图和气泡图的替代选项。

哒哒!!

在空间图上绘制多个柱状图。图像由作者提供。

5. 使用折线图处理时间序列数据

实际上,我们拥有的年度数据是时间序列数据。我们可以应用多条折线图来表示随时间变化的情况。其他位置的数据轮廓可以局部展示,以便于阅读并帮助比较数值。

这种方法可以作为本文中饼图的替代选项,因为它有助于关注每个位置的数据,同时能够与其他数据进行比较。

在空间图上绘制多条折线图以表示时间序列数据。图像由作者提供。

总结

首先,让我们总结一下本文提到的每个图表。

  1. 区域图

  2. 气泡图

  3. 饼图

  4. 柱状图

  5. 折线图

如果我们仔细查看这些图表,大多数只是普通图表,没有什么复杂的。主要思想是应用它们的简洁性来展示复杂的情境:多个位置随时间的同步数据值变化。

“从杂乱中寻找简洁” — 阿尔伯特·爱因斯坦

最后,我非常确定对于这种数据可以有其他图表。本文的主要目的是提供一些思路,文中解释的图表只是示例。

如果您想分享或有任何建议,请随时留言。

感谢阅读。

这里有一些我关于数据可视化的文章,您可能会感兴趣:

  • 使用 Python 处理多变量分类数据的 7 种可视化方法 (link)

  • 使用 Python 处理多个时间序列数据的 8 种可视化方法 (link)

  • 7 个使用 Python 的可视化图表,用于表达排名随时间的变化 (link)

  • 9 个使用 Python 的可视化图表,用于显示比例或百分比,替代饼图 (link)

  • 9 个使用 Python 的可视化图表,比条形图更吸引注意 (link)

参考文献

生成型 AI 改变了公司处理数据的方式(以及没有改变的方式)

原文:towardsdatascience.com/5-ways-generative-ai-changes-how-companies-approach-data-and-how-it-doesnt-6e87c1f3c41?source=collection_archive---------8-----------------------#2023-08-10

来自风险投资、Snowflake 等领域的专家讨论了生成型 AI 如何惠及数据团队以及他们必须解决的挑战。

Michael SegnerTowards Data Science Michael Segner

·

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

--

图片由作者提供。生成工具为 DiffusionBee

生成型 AI 并不是一个新概念。它已经研究了数十年,并在有限的范围内应用。直到 2022 年底,ChatGPT 震撼了我们的集体意识。

生成意大利面条食谱与在企业中融入生成式人工智能能力或将大型语言模型(LLMs)集成到数据工程工作流程中是完全不同的过程。

变革正在到来,但这对组织如何处理数据以及仍需克服的障碍有什么影响?为了解答这个问题,我们召集了:

他们的经验包括创造新技术、资助大量生成式人工智能初创公司,并帮助成千上万的客户利用人工智能从数据中获得更多价值。

尽管他们对“生成式人工智能有多少是炒作”的估计从 50% 到 20% 不等,但每位小组成员都同意,这项变革性技术今天有实际应用具有巨大的潜力。所以让我们看看一些反复出现的主题。

本文内容:

5 个生成式人工智能的好处

  • 1- 增加数据可访问性

  • 2- 从数据和图表分析中提取洞察

  • 3- 将非结构化数据投入使用

  • 4- 加速数据管道开发

  • 5- 降低成本(也许还有收入生成)

5 个生成式人工智能的挑战

  • 1- 幻觉、可靠性和信任

  • 2- 解决自身问题

  • 3- 安全性和隐私

  • 4- 我们在基础设施和解决方案方面还处于早期阶段

  • 5- 技能集变化

5 个生成式人工智能的好处

在生成式人工智能方面,很难将事实与虚构、实际应用与虚假宣传分开。以下是生成式人工智能在今天和不久的将来为组织和数据团队带来价值的一些当前和可能的方式。

#1- 增加数据可访问性

生成式人工智能在数据领域的最简单应用?让非技术用户输入自然语言提示,从而生成 SQL 查询以检索特定数据点的能力。

托马什说:“文本到 SQL 是无处不在的。OpenAI 正在开发它。你可以在商业智能层面看到它,也可以在数据探索工具中看到它。我认为这是非常简单的[果实]。”

自动化 SQL 查询可以彻底民主化非技术用户的数据访问,从而加速价值实现。

利奥尔说:“我认为人工智能为那些不熟悉数据的人打开了获取有意义数据和洞察的可能性。而且即使是那些知道 SQL 的人,它也将加速这一过程。所以我们将把数据交到更多人的手中。”

数据平台如 Snowflake 正在投资于生成式人工智能如何不仅生成 SQL,还重新构想用户-数据体验。Kristen 谈到了数据云中一个较为实验性的功能,该功能目前正在开发中,旨在将 LLMs、SQL 查询和数据可视化连接起来。

“价值链可能需要很长时间才能生成正确的数据,然后获得令人满意的最终用户体验。这是推送通知吗?是仪表板吗?是商业智能报告吗?”她说。“我对人工智能能够简化这些步骤,并让最终用户能够更快地垂直进入堆栈并获得[更好的]最终体验感到非常兴奋……虽然现在还不是,但我认为它会到来。”

#2- 从数据和图表分析中提取洞察

图表和分析可能很快由生成式人工智能完成。照片由Chris Liverani拍摄,来源于Unsplash

生成式人工智能不仅可以在查询和可视化数据的基础上产生影响,还可以迈出下一步,从中提取洞察。实际上,这个过程已经被 Tomasz 使用了。

“ChartGPT 是一个专注于图表数据的专门产品。如果有人玩过[ChatGPT] 代码解释器,那真的很疯狂,”他说。“你可以将一家上市公司的 Excel 电子表格上传到代码解释器中,要求它总结‘Google 财报后的状态’,它将产生一些相当重要的洞察。”

#3- 发挥非结构化数据的作用

我们所有的专家小组成员对生成式人工智能能够使数据团队和组织从非关系型来源中提取价值的潜力感到兴奋。

“世界上有很多非结构化数据。文本和图像需要专门的技能和工具来分析,这[受限于资源],”Lior 说。“你可以想象生成式人工智能从这些文本和图像数据集中提取结构,并像我们对待关系型数据一样加以利用。”

Tomasz 将这一过程称为“信息开采”。

“在 Theory [他的风险投资公司],我们收集了大约 10,000 份有关初创公司的文档……大约两周前,我们制作了第一份关于被称为生成式 RPA 的领域的投资备忘录总结。这些内容完全是通过大量文本文件进行信息开采产生的,”他说。

Snowflake 和其他数据平台正在推出将加速数据团队实现这一目标的功能。

文档 AI 是一个不断增长的项目,源于我们收购 Applica,其使用案例主要集中在文档阅读和解读上,”她说。“这可能用于人力资源的采购合同等……我遇到的客户都在处理一堆[数据],他们希望从中获得价值。”

文档 AI 在上面展示的最新 Snowflake Summit 上首次推出。照片由作者拍摄。

#4- 加速数据管道开发

生成式 AI 可能会或不会彻底改变数据管道架构,但它肯定会使数据工程师更快地构建和部署它们。

Lior 说:“生成 AI 已经非常擅长编写 SQL、Python、Scala 或任何你用于构建管道的语言。因此,我们将看到生成 AI 使工程师在构建这些解决方案时变得更有效、更快速。” “我不确定它是否能提高 10 倍的效率……但它确实提高了 20% 或 30% 的效率,这对很多团队来说是有意义的。”

然而,Lior 迅速指出,数据建模和理解数据源如何组合在一起不太可能由 AI 完成,他说:“我认为在可预见的未来,这仍然会由人类完成。”

#5- 降低成本(也许还包括收入生成)

这是一个削减成本的环境,许多组织正在优先考虑生成式 AI 如何帮助这些计划。

Kristen 说:“有两种方法可以改善盈利能力。一种是从收入方面入手,这更具外部导向,并且你会遇到很多问题。” “另一种[改善盈利能力的方法]是从内部降低成本……很多人问,‘我们如何利用 AI 降低内部成本?’然后,希望能从中获得很多经验教训,帮助建立更多面向外部的、生成收入的使用案例。”

5 个生成式 AI 挑战

既然我们都陶醉于 AI 的可能性,是时候清醒面对一些非常现实的挑战和限制。这些包括:

#1- 幻觉、可靠性与信任

现在我们已经相当清楚大型语言模型会产生幻觉,换句话说,就是自信地给出错误的结果。这可能会很有挑战性,因为我们经常将经过深思熟虑的论点与正确的论点混淆

Tomasz 说:“我们已经看到了一些挑战,无论是幻觉还是在非常特定领域中让生成 AI 工作的挑战……我认为这些问题仍然是显著的挑战。”

但这不仅仅是幻觉。与分析仪表盘和机器学习应用一样,生成型 AI 模型的可靠性和可信度仅取决于其访问或训练的底层数据。这些问题并不新鲜,但它们会变得更加普遍,并需要更具扩展性的解决方案。

Lior 说:“选择使用的数据集,如何验证它,如何清理它,以及如何将其整合起来,以回答业务问题[当你有] 更少分析师参与时,将加剧当前存在的一些治理问题。如何识别哪些数据集是值得信赖的,哪些是有用的,以及如何拥有正确的元数据和文档,以使数据对模型有用?”

底层数据堆栈还将作为生成型 AI 理解底层数据的关键指南。

Kristen 说:“如果你要求模型根据它对世界其他部分的了解来解释你的业务,那么世界其他部分并不了解你如何解释你的业务。你在哪里将业务逻辑注入到服务于每个后续层的数据堆栈中?我认为在数据堆栈的基础层中,制定一些关于你的业务的关键规则,无论是实体相关还是关系嵌入的,都将有利于[模型的速度和准确性]。”

#2- 解决自身问题

Tomasz 分享了关于将生成型 AI 用于更广泛任务的相关经验:

他说:“我在准备一个演示文稿,想要绘制一个特定 GitHub 存储库的 GitHub 星标增长图。所以我需要构建两个或三个代码块。于是我让机器人生成一个爬虫来获取信息。另一个部分是实际绘图,然后我想自动化它,以便我可以给它一个存储库列表进行处理。” “它很难将这样一个大块的问题分解成单独的任务,然后再构建它。”

他还对生成型 AI 模型在犯错后容易陷入困境表示了沮丧。

他说:“[当] 它走到一个错误的路径上,代码无法执行时,它很难忘记所犯的错误,它想继续改进,你必须重新启动会话。它有非常、非常持久的记忆。”

#3- 安全性和隐私

这可能是 Tomasz 认为生成型 AI 广泛采用的最大障碍。

他说:“许多[银行和财富 2000 强公司]已经完全封锁了[生成型 AI]。我认为他们中的许多人在等待 AI 产品在 Azure VPC 内发布,以便他们可以控制它。在云中,我们转向了多租户,其中多个客户在同一台机器上……对于大型语言模型,客户会接受这种情况吗,还是我们实际上需要每个客户一个大型语言模型?”

行业在应对这些问题的初期阶段,但到目前为止,解决这些安全和隐私挑战的主要方式有四种:

  • 禁止它

  • 定期告诉 LLM“忘记我所有的数据,并且不要用它来进行训练”

  • 使用中间层来拦截个人身份信息

  • 接受风险

解决方案通常由使用案例决定。生成模板化网站的风险远低于处理可能涉及客户信息和信用卡的客户服务。

当然,如果没有提到提示注入和一个相关的 xkcd关于 SQL 注入的讨论,那这次生成 AI 的讨论就不完整了。

#4- 我们在基础设施和解决方案方面还处于早期阶段

应对治理、可靠性、安全性和其他生成 AI 挑战将需要一个当前还不存在的强大基础设施。

利奥说:“在我称之为基础设施和解决方案方面,仍然处于早期阶段。当我们现在谈论生成 AI 时,对大多数人来说,这意味着使用 OpenAI 的 API。少数人可能在玩一些开源模型和向量数据库,但我认为围绕它的工具还是相当早期的。”

分类和访问控制是另外两个挑战,支持基础设施可以提供帮助。

克里斯滕说:“数据分类是一个困难的问题。当你考虑访问控制和大规模访问控制时……你必须已经了解分类。” “我们在这个领域花了很多时间,并且有一些原生于 Snowflake 的功能。”

PII 实际上并不难弄清楚,但我认为关注点在于使人们能够创建自定义分类器。因为宇宙是无限的,如果你[考虑]什么构成商业机密或客户数据,这在公司内部可能是动态的。”

#5- 技能集转变

也许讨论的最长部分集中在对新兴生成 AI 技能集的需求以及部署这些大型语言模型将导致的劳动力转变上。

最大的赢家?数据团队,他们的服务需求将呈指数增长,特别是对不同生成 AI 模型的评估和微调。

托马斯说:“我认为某些[责任]将转移到数据团队,因为他们需要确保输出的准确性以及所使用的解决方案符合适用于该业务的任何法规。” “他们将负责从初步评估到持续维护。因此,我认为数据团队将会增长。”

利奥指出,过去的技术进步表明,不太可能减少数据人员的数量,反而我们将从数据中创造更多的价值。

“我们过去一再见证了软件的发展。我们最终从汇编语言发展到了 Python,这要高效得多。但这是否意味着我们有更少的软件工程师?不,我们有更多的软件工程师,而且世界上有更多的软件,我认为这种趋势在可预见的未来会持续,”他说。

正如克里斯滕所强调的,那种日益增长的需求面临的挑战之一是弥合技能差距。

“我看到的动向就像在…… OpenAI 试图通过让更多人能够使用 SQL 来弥合技能差距。在管理所有基础设施、模型和我们讨论过的 LLM 相关内容时,我觉得那是更深层次的……这不是更多的 SQL 分析师,而是不同的角色。”

数据团队的激动人心的时刻

正如我们的专家小组所明确指出的,生成式 AI 将对组织如何思考和提取数据价值产生重大影响……只不过可能比那些 Twitter X 帖子所暗示的要慢一点。

这是在数据领域的一个激动人心的时刻,有机会解决这些重要的挑战。

感谢阅读! 关注我 在 Medium 上获取更多关于数据工程、数据质量及相关主题的故事。 订阅 以便将我的故事送到您的邮箱。

使用 Python 实现开放封闭原则的 5 种方法

原文:towardsdatascience.com/5-ways-of-implementing-open-closed-principle-with-python-51fd21a90772

数据科学家的面向对象编程原则

Erdem IsbilenTowards Data Science Erdem Isbilen

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

--

照片由 Tim Mossholder 提供,发布于 Unsplash

开放封闭原则(OCP)是面向对象编程五大 SOLID 原则之一。它规定软件实体,如类、模块和函数,应对扩展开放,但对修改封闭。换句话说,你应该能够在不修改现有代码的情况下向软件添加新功能。

OCP 的目标是创建更加灵活且易于维护的软件。通过设计可以扩展而无需修改现有代码的软件,你可以降低引入新错误的风险,并使你的代码更易读和理解。

开放封闭原则如何帮助数据科学家?

虽然 OCP 主要关注软件设计,但它也可以应用于数据科学。数据科学家经常处理需要随着时间更新和修改的大型复杂数据集和模型。通过遵循 OCP,数据科学家可以确保他们的模型在长期内易于扩展和维护。

在数据科学的背景下,“模型”通常指的是现实世界系统或过程的数学或统计表示。模型可以用来做出预测、分类数据或理解变量之间的复杂关系。

例如,数据科学家可能会构建一个机器学习模型来预测基于历史客户数据的客户流失情况。该模型将以过去客户行为的数据集进行训练,并利用这些信息预测哪些客户未来最可能流失。

模型可以采取多种形式,具体取决于解决的问题和可用的数据。在数据科学中,一些常见的模型类型包括回归模型、决策树、神经网络和支持向量机。

具体来说,开闭原则可以在以下方面帮助数据科学家:

  1. 促进模型扩展:通过设计模型以便于扩展,数据科学家可以轻松地向模型中添加新特性和功能,而无需修改原始代码。这可以帮助他们保持模型的最新性和相关性。

  2. 鼓励模块化设计:开闭原则鼓励模块化设计,这可以使得更新和修改模型变得更容易。通过将模型分解为更小、更易管理的组件,数据科学家可以在不影响其他代码的情况下对模型的特定部分进行更改。

  3. 提高可维护性:通过设计封闭于修改的模型,数据科学家可以确保他们的代码更稳定,出错的可能性更小。这可以使得维护和更新模型变得更容易。

在 Python 中实现开闭原则的 5 种方法?

(1) 使用抽象:

在 Python 中实现开闭原则的一种方法是使用抽象来隐藏实现细节并允许扩展而不进行修改。在这个例子中,我们定义了一个抽象基类 DataTransformer,该类定义了一个抽象方法 transform。这个类作为我们未来可能创建的任何数据转换器的抽象。然后我们定义了两个具体的实现:StandardScalerTransformerLogTransformer

from abc import ABC, abstractmethod
import pandas as pd

class DataTransformer(ABC):
    @abstractmethod
    def transform(self, data):
        pass

class DataPipeline:
    def __init__(self, transformers):
        self.transformers = transformers

    def run(self, data):
        for transformer in self.transformers:
            data = transformer.transform(data)
        return data

class StandardScalerTransformer(DataTransformer):
    def __init__(self, mean=None, std=None):
        self.mean = mean
        self.std = std

    def fit(self, data):
        if self.mean is None:
            self.mean = data.mean()
        if self.std is None:
            self.std = data.std()

    def transform(self, data):
        self.fit(data)
        return (data - self.mean) / self.std

class LogTransformer(DataTransformer):
    def transform(self, data):
        return pd.Series(data).apply(lambda x: log(x))

if __name__ == '__main__':
    # Load data
    data = pd.read_csv('data.csv')

    # Instantiate transformers
    scaler = StandardScalerTransformer()
    log_transformer = LogTransformer()

    # Create and run pipeline
    pipeline = DataPipeline(transformers=[scaler, log_transformer])
    data = pipeline.run(data)

DataPipeline 类将一组 DataTransformer 对象作为其构造函数的参数。这使我们可以在不修改类本身的情况下向管道中添加或移除转换器。run 方法依次将管道中的每个转换器应用于数据。

使用这种抽象,我们可以轻松地创建实现 transform 方法的新转换器,并将它们添加到管道中,而无需修改现有的 DataPipeline 类。这展示了开闭原则的实际应用:DataPipeline 类对扩展是开放的(我们可以向管道中添加新转换器),但对修改是封闭的(我们无需修改现有类即可添加新转换器)。

(2) 使用继承和/或组合:

在 Python 中实现开闭原则的另一种方法是使用继承来扩展类的行为。通过创建一个具有良好定义接口的基类,你可以创建新的子类,这些子类继承了该接口并添加新功能。这允许你在不修改原始实现的情况下扩展代码的行为。

我们可以通过组合而非继承在 Python 中实现开闭原则(OCP)。通过定义包含其他对象的对象,数据科学家可以创建更模块化、更易于扩展的代码。

在这个例子中,我们定义了DataAnalyzer基类,其中包含必须由子类实现的抽象方法preprocessanalyze。我们还定义了一个__init__方法,该方法接受要分析的数据。

然后我们定义了三个子类,分别用于分析数值、文本和图像数据。每个子类重写了preprocessanalyze方法,以提供该数据类型的专门功能。

例如,NumericalDataAnalyzer子类包括一个preprocess方法,该方法使用StandardScaler对象对数值数据进行缩放,而TextDataAnalyzer子类包括一个preprocess方法,该方法使用TfidfVectorizer对象对文本数据进行向量化。同样,ImageDataAnalyzer子类包括一个preprocess方法,该方法使用ResNet50对象从图像数据中提取特征。

from abc import ABC, abstractmethod

class DataAnalyzer(ABC):
    def __init__(self, data):
        self.data = data

    @abstractmethod
    def preprocess(self):
        pass

    @abstractmethod
    def analyze(self):
        pass

class NumericalDataAnalyzer(DataAnalyzer):
    def __init__(self, numerical_data):
        super().__init__(numerical_data)
        self.scaler = StandardScaler()

    def preprocess(self):
        self.data = self.scaler.fit_transform(self.data)

    def analyze(self):
        # analyze numerical data here

class TextDataAnalyzer(DataAnalyzer):
    def __init__(self, text_data):
        super().__init__(text_data)
        self.vectorizer = TfidfVectorizer()

    def preprocess(self):
        self.data = self.vectorizer.fit_transform(self.data)

    def analyze(self):
        # analyze text data here

class ImageDataAnalyzer(DataAnalyzer):
    def __init__(self, image_data):
        super().__init__(image_data)
        self.feature_extractor = ResNet50()

    def preprocess(self):
        self.data = self.feature_extractor.extract_features(self.data)

    def analyze(self):
        # analyze image data here

通过使用继承创建专门的子类,并使用组合将专门的对象包含在这些子类中,我们可以一致而灵活地分析每种类型的数据,同时遵循开放封闭原则。如果将来需要支持新的数据类型,我们只需创建一个新的子类,继承自DataAnalyzer并包含适当的专门对象组合。这种方法使我们能够在不修改现有代码的情况下扩展程序,从而更易于维护和重用。

(3) 使用插件:

OCP(开放封闭原则)可以应用于创建 Python 中的插件架构。通过定义一组明确的接口或抽象基类,数据科学家可以允许其他开发者编写插件,扩展其代码的功能而无需修改原始实现。

假设我们有一个脚本在给定的数据集上执行一些数据处理。我们希望能够轻松地添加和移除不同的数据处理步骤作为插件,而无需修改脚本代码。

我们可以使用插件架构将数据处理步骤定义为可以在运行时动态加载的插件。以下是一些示例代码:

import importlib

class DataProcessingPlugin:
    def process_data(self, data):
        pass

class RemoveDuplicatesPlugin(DataProcessingPlugin):
    def process_data(self, data):
        # Remove duplicate rows from the data
        return data.drop_duplicates()

class ImputeMissingValuesPlugin(DataProcessingPlugin):
    def process_data(self, data):
        # Impute missing values in the data using mean imputation
        return data.fillna(data.mean())

def process_data(data, processing_steps):
    # Load plugins dynamically
    plugins = [importlib.import_module(f'plugins.{step}_plugin') for step in processing_steps]

    # Apply each processing plugin to the data sequentially
    for plugin in plugins:
        data = plugin.process_data(data)

    return data

在这个例子中,我们定义了一个DataProcessingPlugin类,该类定义了数据处理插件的接口。我们还定义了两个插件,RemoveDuplicatesPluginImputeMissingValuesPlugin,它们实现了DataProcessingPlugin接口,并分别定义了去除重复行和填补缺失值的自定义逻辑。

我们定义了一个process_data函数,该函数接受数据集和处理步骤列表作为输入。该函数使用importlib模块动态加载与给定步骤对应的每个处理插件,并按顺序将每个处理插件应用于数据,以生成最终的处理数据集。

以这种方式使用插件使我们能够轻松修改和尝试不同的数据处理步骤,而无需修改process_data函数代码。这也使得在不同项目之间共享和重用数据处理代码变得更加容易。

(4) 使用配置文件:

可以通过使用配置文件来控制 Python 程序的行为,从而应用 OCP。通过将配置数据与代码分离,数据科学家可以创建更易于扩展和维护的程序。例如,数据科学家可能会定义一个配置文件,指定机器学习模型的参数,使其他开发人员可以在不修改原始代码的情况下实验不同的参数设置。

这是一个示例:

假设我们有一个客户评论的数据集,我们希望使用各种机器学习模型来分析每个评论的情感。我们希望能够轻松地更换所使用的机器学习模型,而无需更改情感分析脚本的代码。

我们可以使用配置文件来指定使用哪个机器学习模型及其相关超参数。以下是一个示例配置文件:

{
  "model": "logistic_regression",
  "model_params": {
    "C": 1.0
  }
}

在这个示例中,我们使用逻辑回归作为我们的机器学习模型,并指定了 1.0 的正则化参数。

然后,我们的情感分析脚本可以读取这个配置文件,并使用指定的机器学习模型来分析每个评论的情感。以下是一些示例代码:

import pandas as pd
from sklearn.linear_model import LogisticRegression
import json

def load_data(filename):
    # Load customer reviews from CSV file
    data = pd.read_csv(filename)

    # Remove any rows with missing data
    data.dropna(inplace=True)
    return data

def preprocess_data(data):
    # Preprocess customer reviews
    # ...
    return processed_data

def train_model(data, model_name, model_params):
    # Train specified machine learning model on preprocessed data
    if model_name == 'logistic_regression':
        model = LogisticRegression(C=model_params['C'])
    else:
        raise ValueError('Invalid model name: {}'.format(model_name))
    model.fit(data['X'], data['y'])
    return model

if __name__ == '__main__':
    # Load configuration from file
    with open('config.json') as f:
        config = json.load(f)

    # Load data
    data = load_data('reviews.csv')

    # Preprocess data
    processed_data = preprocess_data(data)

    # Train model
    model = train_model(processed_data, config['model'], config['model_params'])

    # Use model to analyze sentiment of each review
    # ...

在这个示例中,我们定义了一个load_data函数来从 CSV 文件中加载客户评论,一个preprocess_data函数来预处理数据以供机器学习模型使用,以及一个train_model函数来在预处理数据上训练指定的机器学习模型。

我们使用json.load方法读取配置文件,并将指定的机器学习模型及其超参数传递给train_model函数。这使我们可以通过简单地修改配置文件来轻松更换所用的机器学习模型,而无需更改情感分析脚本中的任何代码。

以这种方式使用配置文件来指定参数是在数据科学中一种常见的模式,因为它允许轻松修改和实验,而无需修改代码。

(5) 使用依赖注入:

可以通过使用依赖注入来动态创建和配置对象,从而应用 OCP。

在这个示例中,我们定义了三个用于数据清理、特征工程和机器学习的类。每个类都将策略作为依赖项,在创建类的实例时注入。

class DataLoader:
    def __init__(self, filename):
        self.filename = filename

    def load_data(self):
        # Load data from file
        data = pd.read_csv(self.filename)
        return data

class DataCleaner:
    def __init__(self, strategy):
        self.strategy = strategy

    def clean_data(self, data):
        # Clean data using specified strategy
        cleaned_data = self.strategy.clean(data)
        return cleaned_data

class FeatureEngineer:
    def __init__(self, strategy):
        self.strategy = strategy

    def engineer_features(self, data):
        # Engineer features using specified strategy
        engineered_data = self.strategy.engineer(data)
        return engineered_data

class Model:
    def __init__(self):
        self.model = RandomForestClassifier()

    def train(self, X, y):
        # Train machine learning model on preprocessed data
        self.model.fit(X, y)

    def predict(self, X):
        # Use trained machine learning model to make predictions
        predictions = self.model.predict(X)
        return predictions

if __name__ == '__main__':
    # Create instances of data cleaning and feature engineering strategies
    cleaning_strategy = RemoveDuplicatesStrategy()
    feature_engineering_strategy = AddFeaturesStrategy()

    # Create instances of data loader, data cleaner, feature engineer, and model
    data_loader = DataLoader('data.csv')
    data_cleaner = DataCleaner(cleaning_strategy)
    feature_engineer = FeatureEngineer(feature_engineering_strategy)
    model = Model()

    # Load data
    data = data_loader.load_data()

    # Clean data
    cleaned_data = data_cleaner.clean_data(data)

    # Engineer features
    engineered_data = feature_engineer.engineer_features(cleaned_data)

    # Train model
    X = engineered_data.drop('target', axis=1)
    y = engineered_data['target']
    model.train(X, y)

    # Make predictions
    predictions = model.predict(X)

我们创建数据清理和特征工程策略的实例,并将它们分别注入到DataCleanerFeatureEngineer类的实例中。这使我们可以轻松地用不同的实现替换数据清理和特征工程策略,而无需修改机器学习脚本中的任何代码。

我们还创建了一个DataLoader类的实例,该实例未注入任何依赖。这是因为数据加载策略不太可能频繁变化,因此不需要将其指定为依赖项。

以这种方式使用依赖注入使我们能够轻松修改和实验不同的数据清洗和特征工程策略,而无需修改机器学习代码。

结论

遵循开放-封闭原则,数据科学家可以创建不仅功能齐全,而且模块化、可扩展且易于维护的代码。这有助于确保我们数据科学项目的长期有效性,使我们能够更轻松地应对数据科学领域不断变化的需求。

在 Pandas 中应用 if-else 条件语句的 5 种方法

原文:towardsdatascience.com/5-ways-to-apply-if-else-conditional-statements-in-pandas-b9627e5f475b

重新审视 Pandas 基础知识并提升你的数据处理技能

My Data TalkTowards Data Science My Data Talk

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

--

图片由 muxin alkayis 提供,来源于 Pixabay

在 Pandas 数据框中创建新列或修改现有列——基于一组 if-else 条件——可能是所有数据处理任务中最常遇到的问题之一。在这篇文章中,我想与大家分享我的笔记本,总结了在 Pandas 数据框中应用 if-else 条件语句的 5 种流行方法,并附有实用的代码片段。为简便起见,我创建了一个小的示例数据集,并将在整个教程中使用它进行演示。

假设我们有一个如下所示的 Pandas 数据框。列 ‘visits_30days’ 显示了客户在过去 30 天内访问网站的次数。我们希望创建一个新列,将这些客户分类为‘非访问者’或‘访问者’(一种二元分类),或者将他们分类为多个类别,如 ‘0 次访问’,‘1–5 次访问’,‘6-10 次访问’等。我们将把这个新列命名为 ‘visits_category’。

作者提供的图片

方法 1:使用 numpy.where() 函数

numpy.where() 函数是一个优雅而高效的 Python 函数,你可以用它来基于‘真’或‘假’的 二元 条件添加新列。语法如下:

np.where(condition, value if condition is true, value if condition is false)

将该语法应用于我们的数据框中,我们的代码将如下所示。新列 ‘visits_category’ 的值为 ‘Yes’ 或 ‘No’,这取决于 ‘visits_30days’ 列的值是否等于 0。

import pandas as pd
import numpy as np
df['visits_category'] = np.where(df['visits_30days']== 0, 'YES', 'NO')

图片来源于作者

方法 2:使用 lambda 函数

np.where() 一样,当你需要基于简单的二元 if-else 条件添加列时,lambda 函数是另一个极好的选择。使用 lambda 函数的通用代码结构如下:

df['new column name'] = df['column name'].apply(lambda x: 'value if condition is true' if x condition else 'value if condition is false')

对于我们的具体示例,代码将如下所示:

df['visits_category'] = df['visits_30days'].apply(lambda x: 'YES' if x == 0 else 'NO')

方法 3:使用 numpy.select() 函数

现在,让我们进入一个更复杂的场景。假设我们想为‘visits_category’创建多个类别,并涉及多个条件。你不能通过使用 np.where() 实现它。相反,你可以使用 np.select() 来应用更复杂的条件。

首先,创建一个 Python list,按顺序指定你的条件,以确定哪些条件需要首先执行、其次执行等。

#Create a python list to specify all the conditions
conditions = [
    (df['visits_30days'] == 0),
    (df['visits_30days'] > 0) & (df['visits_30days'] <= 5),
    (df['visits_30days'] > 5) & (df['visits_30days'] <= 10),
    (df['visits_30days'] > 10)
    ]

接着,创建一个包含我们想要分配给每个条件的值的 Python list。确保每个值对应于第一步中创建的条件。

#create a python list of values to be assigned to the conditions
values = ['0 visits', '1-5 visits', '6-10 visits', '>10 visits']

最后,使用 np.select() 创建新列,传入之前创建的两个 lists 作为参数。

# use np.select() to create a new column
df['visits_category'] = np.select(conditions, values)

将这三段代码结合在一起,你会得到以下结果:

df['visits_category'] = np.select([
    (df['visits_30days'] == 0),
    (df['visits_30days'] > 0) & (df['visits_30days'] <= 5),
    (df['visits_30days'] > 5) & (df['visits_30days'] <= 10),
    (df['visits_30days'] > 10)
    ], ['0 visits', '1-5 visits', '6-10 visits', '>10 visits'])

图片来源于作者

方法 4:使用 Pandas apply() 函数

使用这种方法,我们可以首先定义一个指定条件的函数。然后,将该函数应用到列上(axis=1)。

def conditions(row):
    if row['visits_30days'] == 0:
        val = '0 visits'
    elif row['visits_30days'] >0 & row['visits_30days']<=5:
        val = '1-5 visits'
    elif row['visits_30days'] >5 & row['visits_30days']<=10:
        val = '5-10 visits'  
    elif row['visits_30days'] >10:
        val = '>10 visits'  
    else:
        val = 'NA'
    return val

#Apply the function to each data point in the data frame
df['visits_category']= df.apply(conditions, axis=1)

方法 5:使用 DataFrame.loc()

Pandas DataFrame.loc() 通过标签选择 DataFrame 中的行和列。例如,在下面的代码中,第一行代码选择了 dataframevisits_30days 值为零的行,并将 ‘0 visits’ 分配给新列 ‘visits_category’,仅对符合该特定条件的行进行操作。然后,你对其他条件也做同样的操作。

df.loc[(df['visits_30days'] == 0), 'visits_category'] = '0 visits' 
df.loc[(df['visits_30days'] > 0) & (df['visits_30days'] <= 5), 'visits_category'] = '1-5 visits' 
df.loc[(df['visits_30days'] > 5) & (df['visits_30days'] <= 10), 'visits_category'] = '5-10 visits'  
df.loc[(df['visits_30days'] > 10) , 'visits_category'] = '>10 visits' 

学习和总结最常见的 pandas 数据清洗技术一直是我日常数据科学家工作中的有趣且有帮助的练习。数据清洗是数据科学家日常工作中的一个重要环节,占据了数据分析项目近 80% 的时间。身边有一个记事本,将你最喜欢的代码片段分类和总结,是提高工作效率的好方法。希望你喜欢本教程,谢谢阅读。

数据来源: 本教程使用的样本数据集由作者创建,用于演示目的。

你可以通过这个 推荐链接 注册 Medium 会员(每月 5 美元)来解锁我所有的写作内容和 Medium 的其余部分。通过这个链接注册,我将获得你会员费的一部分,而你无需额外支付费用。谢谢!

获取你下一个数据项目的有趣数据集的 5 种方法(非 Kaggle)

原文:towardsdatascience.com/5-ways-to-get-interesting-datasets-for-your-next-data-project-not-kaggle-71cf76eef64b

对 Kaggle 和 FiveThirtyEight 感到厌倦了吗?这里是我用来获取高质量和独特数据集的替代策略。

Matt ChapmanTowards Data Science Matt Chapman

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

--

图片来源 Efe KurnazUnsplash

一个伟大的数据科学项目的关键是一个优秀的数据集,但找到优质数据远比说起来容易。

我记得在我读数据科学硕士学位时,一年多前。整个课程中,我发现想出项目想法很简单——真正让我困扰的是找到好的数据集。我会花几个小时在互联网上搜索,拼命想找到有用的数据源,但始终无功而返。

从那时起,我在方法上取得了很大进展,在这篇文章中,我想与您分享我用来寻找数据集的 5 种策略。如果你对 Kaggle 和 FiveThirtyEight 这样的标准来源感到厌倦,这些策略将使你能够获得独特且更加贴合你特定用例的数据。

1. 制作自己的数据

没错,无论你信不信,这确实是一个合法的策略。它甚至有一个花哨的技术名称(“合成数据生成”)。

如果你正在尝试一个新想法或有非常具体的数据需求,制作合成数据是获取原创且量身定制的数据集的绝佳方式。

比如,假设你正在尝试建立一个客户流失预测模型——一个可以预测客户离开公司的可能性模型。流失是许多公司面临的一个相当常见的“运营问题”,解决这样的问题是展示你可以利用机器学习解决商业相关问题的绝佳方式,就像我之前所说的:

## 如何找到独特的数据科学项目创意,使你的作品集脱颖而出

忘掉 Titanic 和 MNIST:选择一个独特的项目来提升你的技能,帮助你在众人中脱颖而出

towardsdatascience.com

然而,如果你在网上搜索“客户流失数据集”,你会发现(截至写作时)公开可用的主要数据集只有两个: 银行客户流失数据集电信流失数据集。这些数据集是一个很好的起点,但可能无法反映其他行业所需的数据类型。

相反,你可以尝试创建更符合你要求的合成数据。

如果这听起来太美好而不真实,这里有一个我用一个简短提示创建的示例数据集,这个提示是发给 ChatGPT 的老方法:

作者提供的图片

当然,ChatGPT 在创建数据集的速度和规模上有一定的限制,所以如果你想扩展这项技术,我建议使用 Python 库 faker 或 scikit-learn 的 sklearn.datasets.make_classificationsklearn.datasets.make_regression 函数。这些工具是以编程方式快速生成巨大数据集的绝佳方法,非常适合构建概念验证模型,而不必花费大量时间寻找完美的数据集。

实际上,我很少需要使用合成数据创建技术来生成完整数据集(而且,正如我稍后将解释的那样,如果你打算这样做,你应该谨慎)。相反,我发现这是生成对抗性示例或向数据集中添加噪声的一个非常好的技术,使我能够测试模型的弱点并构建更强健的版本。但是,无论你如何使用这项技术,它都是一个非常有用的工具。

向公司请求他们的数据(礼貌地)

创建合成数据是当你找不到所需数据类型时的一个不错的解决办法,但明显的问题是,你无法保证这些数据能很好地代表真实生活中的人群。

如果你想确保你的数据是现实的,最好的方法是,出乎意料的是……

… 实际上去寻找一些真实数据。

一种方法是联系可能持有这些数据的公司,询问他们是否有兴趣与您分享一些数据。风险提示,没有公司会给你高度敏感的数据,尤其是如果你计划将其用于商业或不道德的目的。这会显得非常愚蠢。

然而,如果你打算将数据用于研究(例如,用于大学项目),你可能会发现公司愿意提供数据,只要这是在互惠互利的联合研究协议的背景下。

我所说的是什么意思?实际上很简单:我指的是一种安排,即他们向你提供一些(匿名/去敏感化的)数据,而你利用这些数据进行对他们有一定益处的研究。例如,如果你有兴趣研究客户流失建模,你可以提出一个比较不同流失预测技术的提案。然后,将提案与一些公司分享,并询问是否有可能合作。如果你足够坚持并广泛接触,你可能会找到愿意提供数据的公司只要你与他们分享你的发现,这样他们就可以从研究中获得益处。

如果这听起来太好了以至于难以置信,你可能会惊讶地发现这正是我在硕士学位期间所做的。我联系了几家公司,提出了一个如何利用他们的数据进行对他们有益的研究的提案,签署了一些文件以确认我不会将数据用于其他目的,并使用一些真实世界的数据进行了一个非常有趣的项目。确实可以做到这一点。

我特别喜欢这种策略的另一点是,它提供了一种锻炼和发展一套广泛技能的方式,这些技能在数据科学中非常重要。你必须善于沟通,展示商业意识,并成为管理利益相关者期望的高手——这些都是数据科学家日常生活中必不可少的技能。

请让我获取你的数据。我会表现良好的,我保证!图片由Nayeli Rosales提供,来源于Unsplash

查阅学术界存储其期刊文章代码的代码库

很多用于学术研究的数据集并没有发布在像 Kaggle 这样的平台上,但仍然对其他研究人员公开可用。

寻找类似数据集的最佳方法之一是查看与学术期刊文章相关的资料库。为什么?因为很多期刊要求其投稿者公开基础数据。例如,我在攻读硕士学位期间使用的两个数据源(Fragile Families 数据集和 Hate Speech Data 网站)在 Kaggle 上没有提供;我通过学术论文及其相关代码库找到了这些数据源。

如何找到这些资料库?实际上非常简单——我从打开 paperswithcode.com 开始,搜索我感兴趣领域的论文,并查看可用的数据集,直到找到看起来有趣的内容。根据我的经验,这是一种非常巧妙的方式来找到那些没有被 Kaggle 群众反复使用的数据集。

BigQuery 公共数据集

老实说,我不知道为什么更多人不利用 BigQuery 公共数据集。这里真的有数百个数据集,涵盖从 Google 搜索趋势到伦敦自行车租赁,再到大麻基因组测序的所有内容。

我特别喜欢这个来源的一个原因是,这些数据集在商业上非常相关。你可以告别像花卉分类和数字预测这样的小众学术话题;在 BigQuery 中,有关于广告效果、网站访问和经济预测等现实世界商业问题的数据集。

很多人避开这些数据集,因为它们需要 SQL 技能来加载。但即使你不知道 SQL,只会 Python 或 R 等语言,我仍然建议你花一两个小时学习一些基本的 SQL,然后开始查询这些数据集。上手并不需要很长时间,这确实是一个高价值数据资产的宝库。

要使用 BigQuery 公共数据集中的数据集,你可以按照 这里 的说明注册一个完全免费的账户并创建一个沙箱项目。你无需输入信用卡信息或类似的东西——只需提供你的名字、电子邮件、项目的基本信息即可。如果以后需要更多计算能力,你可以将项目升级为付费项目,访问 GCP 的计算资源和高级 BigQuery 功能,但我个人从未需要这样做,发现沙箱环境已经足够了。

尝试使用数据集搜索引擎

我的最后一个建议是尝试使用数据集搜索引擎。这些工具在过去几年才刚刚出现,它们使得快速查看现有数据变得非常容易。我最喜欢的三个是:

根据我的经验,使用这些工具进行搜索往往比使用通用搜索引擎更有效,因为你通常可以获得有关数据集的元数据,并且可以根据使用频率和发布日期对其进行排名。如果你问我,这是一种相当巧妙的方法。

感谢阅读!希望你觉得这 5 种策略有帮助,如果你有任何反馈或问题,请随时联系我 😃

还有一件事——你能成为我那 1% 的一员吗?

在 Medium 上不到 1% 的读者点击我的“关注”按钮,因此无论是在 Medium、Twitter 还是 LinkedIn,你做出这样的举动对我而言意义重大。

如果你希望获得对我所有故事(以及 Medium.com 上其他内容)的无限访问权限,你可以通过我的 推荐链接 以每月 $5 注册。相比于通过普通注册页面注册,这不会额外增加你的费用,并且有助于支持我的写作,因为我会获得一小部分佣金。

50,000 家零售店关店在即:拥抱数据驱动和以客户为中心的方法

原文:towardsdatascience.com/50-000-more-retail-store-closures-on-the-horizon-embracing-a-data-driven-approach-cd016c2174a1?source=collection_archive---------20-----------------------#2023-01-12

盈利店铺队伍优化指南

如何分析服务区、店铺网络、销售转移和收购影响

马丁·莱特纳Towards Data Science 马丁·莱特纳

·

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

--

图片由 Tim Mossholder 提供,来源于 Unsplash

在过去的一年里,许多零售商因客户偏好变化、竞争加剧和运营费用上升而不得不关闭大量门店。一些宣布重大店铺关闭的零售商例子包括:

  1. J.C. Penney — 这家百货公司连锁店已宣布计划在未来几年关闭 200 多家门店,因为它在与在线零售商竞争中挣扎。

  2. 梅西百货 — 这家百货公司连锁店已宣布计划在未来几年内关闭 100 多家门店,以适应消费者行为的变化。

  3. 西尔斯 — 这家陷入困境的零售商已宣布计划关闭 200 多家门店,努力扭转业务局面。

  4. 古驰 — 这家服装零售商已宣布计划关闭约 200 家门店,以精简运营。

  5. 维多利亚的秘密 — 这家内衣零售商已宣布计划关闭约 250 家门店,因为面临来自在线零售商的激烈竞争。

这些只是过去一年中宣布重大店铺关闭计划的一些零售商例子。在此期间还有许多其他零售商也关闭了门店。分析师估计,仅在美国,短期内我们将看到超过 50,000 家门店的关闭。与其依赖直觉和 Excel 技能,不如采用以数据驱动和客户为中心的方法来提高门店连锁的盈利能力。

如何决定关闭哪些门店?

我通常看到的情况是,财务团队根据盈利能力对各个门店进行排名。然后,他们开始关闭那些盈利能力最差的门店,假设他们可以迅速谈判退出租约,因为这些门店在资产负债表上负担很重。

这很有道理。或者说有没有其他方法?一种包括客户视角的方式,而不仅仅是关于汇总销售和利润的方式。

让我解释一下。当你关闭一家门店时,客户有几种选择。他们可能会去距离较近的邻近门店;他们可能会去竞争对手那里;他们可能会去你的在线商店;他们可能会完全停止购买这一类别的商品而节省开支,或者上述几种情况的组合。这些选择主要集中在现有客户基础上,涉及客户留存。你跟上了吗?大多数门店销售来自现有客户基础,特别是你的忠实购物者,这毫无疑问。然而,现实是,门店对于零售商或百货商店的客户获取策略也很重要,因为门店能创造品牌认知并作为营销工具。那么,如何将这一点考虑到门店关闭的方程中呢?

回顾一下,我们讨论了两个要素 — 门店关闭对客户留存和客户获取的影响。

突然间,这听起来更像是一个数据科学问题而非财务排名练习。

摄影作品由 Joshua Sortino 提供,来自 Unsplash

数据驱动与以客户为中心的方法

在您采取这种方法为您的 CFO/COO 制定全面建议时,有几个事项需要考虑。

  • 服务区:如何定义围绕我的商店的重要区域

  • 商店网络:商店之间的关系是什么

  • 预测客户销售转移

  • 预测获取新客户的影响

  • 综合分析(1 年、3 年影响,排名)

  • 测量与模型改进

服务区/贸易区

服务区是指某个特定业务接收客户的地理区域。服务区的定义基于各种因素,如距离、交通可达性和市场需求。它们对于房地产开发、分析和业务规划至关重要。

服务区的计算方式:1.) 在传统方式中,人们会在商店周围画一个一定英里半径的圆,比如三英里,圆内的区域都会被视为服务区。请不要这样做。2.) 第一种方法的进阶方式是使用驾车时间来定义服务区。例如,以您的商店为中心的 30 分钟驾车时间就是服务区。

以客户为中心的建议: 使用过去 12 或 24 个月在商店购物的客户 (根据您的业务模型,您需要自行判断时间范围),并将他们的居住地点进行映射,例如,按人口普查区块组级别。当您完成后,您将发现,区域可能会非常大,通常覆盖美国的大部分区域。缩小范围的一种方法是应用优化函数,使用多边形并努力最小化区域的大小,同时保持累计销售百分比尽可能大。我发现,在一个最优解中,累计销售捕捉率通常在 70%到 80%之间。好了,您现在有了所有商店的定制服务区。

商店网络

不要再独立对待每个商店,以决定如何以及在哪里扩展或缩减商店网络。了解商店之间的连接超越服务区是做出更聪明优化决策的关键。

以客户为中心的建议: 从您的目标商店的客户基础开始。然后查看这些客户还在其他商店(包括电子商务)购物的情况。根据您商店在特定地理区域的分布密度,您可能会对有多少客户在多个地点购物感到惊讶。通常,这些客户是您更忠诚且高价值的客户。

您会发现有三组客户:

  1. 专门在这家商店购物的客户

  2. 花大部分钱在这家店而在其他店(包括电子商务)花较少的钱的客户

  3. 在这家店花少量钱而在其他店(包括电子商务)花最多钱的客户

因此,你现在有了基于客户消费行为的忠诚度店铺,并且你也了解他们还在哪里购买你的产品。这是网络的基础。不过要注意: 很可能需要修剪你的网络以保持其规模,否则它们可能会迅速膨胀,覆盖到所有店铺。这是因为客户为了业务或休闲旅行到其他州,并在他们不会在此行之外出现的地点购买商品。你可以通过建立最低$贡献规则来轻松做到这一点。

现在,你应该拥有覆盖整个店铺网络的各种规模,从孤立区域的单店网络到包含十几家店铺的更大网络。

Omar Flores 的照片,来自 Unsplash

预测店铺关闭对销售转移和获取新客户的影响

在店铺关闭后,客户保留依赖于其他渠道(如在线或实体店)的便利性和质量、客户对公司及其产品的忠诚度以及市场竞争。

你现在如何预测关闭一家店后会发生什么? 我过去最成功的方法是使用决策树或回归分析。零售的一个优点是,当你查看历史时,你会发现由于各种原因,已经有了相当数量的店铺关闭。首先,你需要解决客户保留问题。从高层次来看,以下是我发现最有效的两种方法。

  1. 决策树:该模型使用树状结构根据一系列二元分裂做出选择,每个分裂由特定预测变量的值决定。美妙之处在于,你已经在完成服务区和店铺网络计算时建立了关键特征。现在,你可以通过增加额外的特征来增强它,例如特定服务区的竞争情况。

  2. 回归分析:该模型通过一个或多个预测变量来预测连续结果(在我们的例子中是客户保留)。如上所述,你已经创建了基本特征,因此你的工作有了良好的起点。

关于收购的工作可能会显得更为晦涩。我想,你可以以门店的服务范围为起点。由于该区域可能与其他门店的服务范围重叠,你需要将这些重叠部分扣除,以获得那些不会从实体店角度得到支持的区域来帮助你的收购工作。当你查看趋势时,你可以了解它对整体连锁的相对重要性,以及你是否应该建立一个数字化支持的战略,以抵消在零售位置运营成本的一小部分下获取新客户的损失。

结束语

通过综合上述信息,包括如租赁条款等房地产数据以及关于竞争对手和主力店的市场情报,我们可以通过合理化我们的门店网络来战略性地优化短期和中期的盈利能力。当然,当你将这些信息整理成一个吸引人的前端,并将其打包成一个产品,供你的财务和房地产团队使用时,附加分数会更高。将强大的产品感知作为你方法的一部分,将帮助获得利益相关者的支持,并对你的业务产生实际影响。

一如既往,期待听到你的想法、经验和评论

想要联系?

图片由 Heidi Fin 提供,来源于 Unsplash

Tableau 中的 6 种高级可视化

原文:towardsdatascience.com/6-advanced-visualizations-in-tableau-71ca97fb10a9?source=collection_archive---------1-----------------------#2023-12-06

Tableau 中高级可视化的概述,包括逐步示例

Payal PatelTowards Data Science Payal Patel

·

关注 发布于 Towards Data Science ·17 分钟阅读·2023 年 12 月 6 日

--

作者提供的图片:Tableau 中的高级可视化

Tableau 是一个数据可视化工具,用于创建数据可视化、仪表板和故事。当我第一次开始使用这个工具时,我经常使用Show Me功能来创建数据可视化。这个功能允许用户创建常见的可视化类型,例如柱状图和折线图。

作者提供的图片:Tableau 的 Show Me 功能

尽管标准可视化效果很好,但有时需要高级技术。最近,我创建了一个仪表盘,用于可视化《鲨鱼坦克》中的交易。以下是该仪表盘中使用的一些高级可视化技术,并附有逐步指导,教你如何重新创建这些可视化——无论是使用相同的数据集还是不同的数据集!这个列表也是学习不同可视化类型和脑力激荡如何将它们应用于其他工具的好方法!

本文介绍了以下 6 种数据可视化技术。

  1. 圆环图

  2. 桑基图

  3. 词云

  4. 棒棒糖图

  5. 径向图

  6. 嵌套条形图

最终仪表盘可以在 此处查看。有关数据集的更多信息,请访问 此处

注意:一些可视化使用了一个字段, 记录数。在创建任何可视化之前,创建一个新的字段“记录数”,并将其值设置为 1。要查看包含所有计算字段的数据集,可以从上述仪表盘下载数据集,或点击此处下载数据集

圆环图

圆环图与饼图一样,显示了部分与整体的关系。它们的不同之处在于圆的中心有一个孔,就像一个甜甜圈。

鲨鱼坦克仪表盘中的圆环图显示了总的提案数,按成交或未成交进行分段。

要创建这个圆环图,首先在新的工作表中创建一个饼图。

数据面板中,将记录数(即提案数)添加到架上,将成交情况添加到架上。

注意:确保将“成交情况”度量转换为维度,如下图所示。

作者提供的图像:“记录数”度量在列架上,成交情况维度在行架上

显示选项卡中选择饼图可视化类型。这将生成圆环图的基本可视化。

作者提供的图像:用于开发圆环图的基本饼图

架上添加两个新度量。将AVG(0)架上输入两次。(即,AVG(0) AVG(0))。添加到架后,你将看到两个饼图。

作者提供的图像:两个饼图,是AVG(0) AVG(0)添加到行架后的结果

架上的第二个度量值中选择双轴以重叠两个饼图。

作者提供的图片:选择双轴

两个饼图现在会重叠在一起。调整每个饼图的大小以创建甜甜圈形状。在标记卡下,选择大小,通过将滑块向右移动来增加饼图的大小。

作者提供的图片:调整饼图大小

要创建内环甜甜圈,请调整第二个,即底部的饼图。在标记卡上,导航到第二个度量值的卡片,并移除所有字段。在此情况下,有 4 个字段需要移除。

作者提供的图片:从标记卡中移除额外的字段

移除这些字段将导致中心出现灰色圆圈。这是甜甜圈图的中心。通过在标记卡上调整大小来调整甜甜圈的大小。

作者提供的图片:调整甜甜圈图内圆的大小

使用标记卡来调整甜甜圈图中心的颜色。

作者提供的图片:将甜甜圈图中心的颜色调整为白色,以匹配图表背景

使用第一个饼图,即标记卡下的第二个选项,来调整甜甜圈图的标签和颜色。

作者提供的图片:格式化甜甜圈图上的标签和颜色

通过将一个度量移动到标签,在甜甜圈图的中心添加文本到所有标记卡上。

作者提供的图片:在甜甜圈图中心添加文本

右键单击标题,取消选中显示标题以移除轴标题。

作者提供的图片:移除图表上的标题

右键单击工作表上的任意位置,选择格式以打开格式窗格。使用格式窗格隐藏网格线、调整边框、改变背景颜色等。

作者提供的图片:打开格式窗格以获取更多格式化功能

以下图片显示了经过标记卡格式窗格格式化后的最终甜甜圈图。

作者提供的图片:展示按交易或非交易分类的总投球次数的甜甜圈图

桑基图

桑基图显示了从一个实体到另一个实体的流动。Shark Tank 仪表板中的桑基图展示了从投球者性别(即团队组成)到行业的交易流动。

步骤 1:自联合

要创建桑基图,上传数据集,然后对数据源进行自联合。以下图片显示已加载的数据集,共 1274 条记录和 50 个字段。

作者提供的图片:初始 Shark Tank US 数据集加载到 Tableau 中

要创建自联合,将Shark Tank US 数据集工作表移动到画布上的Shark Tank US 数据集。注意行数从 1274 翻倍至 2548。

作者提供的图片:自联合 Shark Tank 数据集

自联合创建了两个字段——表格名称字段将在本练习中稍后使用。

作者提供的图片:自联合后创建的表格名称列

步骤 2:创建新字段和 Bins

要开发此 Sankey 图,请创建七个计算字段和一个 bin。打开一个新工作表并创建以下计算。注意:下面列出的粗体字段名称,后面跟有相应的计算。

  1. ToPad: 如果 [表格名称] = ‘Shark Tank US 数据集’则为 1,否则为 49 结束

  2. 填充: 右键单击ToPad字段并选择创建Bins 来创建一个新的名为 Padded 的 Bin。将Bin 的大小设置为 1。

  3. t: (INDEX() — 25) / 4

  4. Rank 1: RUNNING_SUM(SUM([记录数]))/TOTAL(SUM([记录数]))

  5. Rank 2: RUNNING_SUM(SUM([记录数]))/TOTAL(SUM([记录数]))

  6. Sigmoid: 1 / (1 + exp(1)^-[t])

  7. 曲线: [Rank 1] + (([Rank 2]-[Rank 1])*[Sigmoid])

  8. 交易大小: WINDOW_AVG(SUM([记录数]))

注意:Rank 2 是 Rank 1 的副本

以下图片展示了如何在 Tableau 中创建计算字段ToPad的示例。

作者提供的图片:为 Sankey 图创建计算字段ToPad的示例

以下图片展示了如何在ToPad上创建一个名为填充的 bin,bin 大小为 1。

作者提供的图片:为 Sankey 图创建 bin 的示例,填充

步骤 3:开发 Sankey 可视化

t移动到架上,将曲线移动到架上,并将填充移动到标记卡上的详细信息

作者提供的图片:将初始字段添加到 Sankey 图的工作表中

行业投手性别表格名称移到标记卡上的详细信息

作者提供的图片:Sankey 图工作表上的标记卡

曲线度量上,选择编辑表格计算,位于架上。

作者提供的图片:在曲线上选择编辑表格计算

对于Rank 1,更新表格计算如下:

  • 选择使用特定维度计算

  • 勾选以下字段:投手性别行业填充表格名称

  • 确保投手性别在顶部,因为它将位于 Sankey 图的左侧

作者提供的图片:Rank 1 表格计算

对于Rank 2,更新表格计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:行业投球者性别填充表名

  • 确保行业在顶部,因为这将位于 Sankey 图的右侧

作者提供的图片:排名 2表计算

对于t,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:填充

作者提供的图片:t表计算

现在,从Columns货架上的t度量的下拉选项中选择编辑表计算。对于t,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:填充

作者提供的图片:t表计算

Sankey 图将在工作表上开始成型。

作者提供的图片:Sankey 图初步标记

通过在Marks 卡片上更改行业投球者性别字段的标记类型来为 Sankey 图添加颜色,如下图所示。将交易规模投球者性别添加到过滤器卡片中,以过滤数据集以显示交易数量。

作者提供的图片:格式化 Sankey 图

编辑 X 和 Y 轴范围。要编辑轴,请右键单击轴并选择编辑轴

对于Y 轴曲线),将范围更改为从 0 开始。

作者提供的图片:更新 Y 轴范围

对于X 轴t),将范围更改为-5 到 5。

作者提供的图片:更新 X 轴范围

隐藏每个轴上的标题,以获得更清晰的视觉效果。(注意:要做到这一点,请右键单击标题,然后取消选中显示标题

交易规模移动到Marks 卡片上的大小,并增加大小以增加图中的线条厚度。这些线条的厚度反映了从投球者性别行业的交易数量。在交易规模上,选择使用 填充,如以下图片所示。

作者提供的图片:将交易规模添加到可视化中

步骤 4:创建 Sankey 图的端点

Sankey 图需要两个条形图,一个用于每个端点(投球者性别行业)。打开一个新的工作表,创建一个条形图。下面的图片显示了投球者性别的条形图。注意:用于 投球者性别 的颜色方案在 Sankey 图的主要部分中使用。

作者提供的图片:按投球者性别显示的交易总数条形图

行业字段重复操作,但这次选择灰色调色板。

作者提供的图片:显示按 行业 划分的总交易数的条形图

现在是时候将所有内容整合在一起了!创建一个新的仪表板,并将三个工作表添加到一起,如下所示。使用 格式窗格 调整背景颜色和边框。

作者提供的图片:组合三个视觉效果的仪表板用于 Sankey 图

第五步:添加仪表板操作

添加以下仪表板操作以启用 Sankey 图的悬停功能。

要添加仪表板操作,从主菜单中选择 仪表板操作添加操作高亮

作者提供的图片:添加仪表板操作

对于 投标者性别,添加以下 高亮操作

作者提供的图片:为 投标者性别 创建高亮操作

这将允许用户在左侧图表上悬停,并查看交易在右侧的去向。

作者提供的图片:投标者性别 高亮操作

行业 创建另一个高亮操作。再次选择 仪表板操作添加操作高亮

输入以下信息以创建 行业悬停高亮操作

作者提供的图片:为行业创建高亮操作

用户现在可以悬停在 行业 上,查看 投标者性别 的交易比例。

作者提供的图片:行业 高亮操作

词云

词云显示文本中最常见的单词,出现频率最高的单词会显示得最大。单词以“云”的形状排列,因此得名词云。词云在某些情况下很有用,例如可视化文本列中的单词频率。

要创建词云,需要一个维度(分类变量)和一个度量(数值变量)。本节展示如何重新创建 Shark Tank Deals 仪表板 的词云。这个词云显示了每个行业的交易数量。

首先,打开一个新的工作表。将 行业 移到 标记卡 上的 文本,将 记录数量(即 投标数量)移到 大小。此外,将 行业 移到 颜色

作者提供的图片:标记卡上的初始维度和度量

这将产生如下所示的热图。要将此可视化转换为词云,在 标记卡 上,将可视化类型更改为 文本

作者提供的图片:在 Tableau 工作表上更改标记类型

这将产生一个词云。使用 标记卡 对格式进行更改。

作者提供的图像:按行业显示交易数量的词云

棒棒糖图表

棒棒糖图表类似于条形图。它们显示不同类别的频率。棒棒糖图表与条形图的不同之处在于视觉展示。虽然条形图使用矩形显示每个类别的值,但棒棒糖图表在每根条形的顶部添加了一个圆圈。这种可视化类型在创建信息图或仪表板时非常有用。

鲨鱼坦克仪表板中的棒棒糖图表显示了每个鲨鱼的交易数量。

首先,将已达成交易度量名称添加到架。通过选择字段上的选项,将已达成交易转换为离散维度。

作者提供的图像:将已达成交易度量名称添加到列架

度量值添加到架上两次。别担心,再经过几步,棒棒糖图表就会开始成型!

作者提供的图像:将度量值添加到行架

在第二个度量值字段上,选择双轴

作者提供的图像:在度量值字段上选择双轴

右击其中一个 y 轴,选择同步轴

作者提供的图像:同步图表轴

度量值卡片上,添加以下字段:巴巴拉投资戴蒙德投资凯文投资洛瑞投资马克投资罗伯特投资。将这些字段的类型更改为度量(总和),如下面的图像所示。从度量值卡片中移除不在此列表中的其他字段。

作者提供的图像:度量值最终列表

已达成交易添加到筛选卡片。筛选条件为已达成交易 = 1,即“已达成交易”。

作者提供的图像:将已达成交易添加到筛选卡片

标记卡片上,将度量值卡片的图表类型更改为条形图,并调整大小。

作者提供的图像:在度量值标记卡片上更改图表类型

在第二个度量值卡片上,将类型更改为圆圈,并使用大小区块调整大小。

作者提供的图像:在标记卡片上调整大小

度量值药丸移动到文本区块,在度量值标记卡片上,以便向图表中的圆圈添加标签。

作者提供的图像:向图表添加标签

右键单击 y 轴,取消选中显示标题,以去除额外的标题。使用格式窗格更改背景颜色、格式化轴,并修改文本标签。要打开格式窗格,右键单击工作表并选择格式

图片作者:打开格式窗格

下图展示了最终的棒棒糖图表。

图片作者:显示每个鲨鱼交易数量的棒棒糖图表

径向图表

径向图表是显示分类信息的另一种方式。径向图表可以是条形图的一个很好的替代选择。

Shark Tank 仪表板中,径向图表按交易中的鲨鱼数量显示交易。要在 Tableau 中创建径向图表,需要一个维度和一个度量。此外,还会在过程中创建 9 个计算字段和 1 个箱子。以下步骤概述了如何重新创建此图表。

第 1 步:自连接

要创建径向图表,上传数据集,然后创建数据源的自连接。下图展示了加载的数据集,总共有 1274 条记录和 50 个字段。

图片作者:初始 Shark Tank US 数据集加载到 Tableau 中

要创建自连接,将Shark Tank US dataset工作表中拖动到画布上的Shark Tank US dataset上。注意行数从 1274 翻倍到 2548。

图片作者:自连接 Shark Tank 数据集

自连接创建了两个字段 — Table Name字段将在此练习中稍后使用。

图片作者:通过自连接过程创建的表名字段

第 2 步:创建新字段和箱子

创建此径向图表需要 9 个计算字段和 1 个箱子。打开一个新的工作表,并按顺序创建以下计算。注意:下面列出的字段名称为粗体字,后跟相应的计算。

  1. Path: IIF([Table Name] = ‘Shark Tank US dataset’, 0, 270)

  2. Path (Bin): — 右键单击Path字段,选择创建箱子,以创建一个名为Path (Bin)的新箱子。将箱子大小设置为 1。

  3. Index-1: INDEX()-1

  4. Total Cat. Deals: WINDOW_SUM(SUM([Number of Records]))/2

  5. Total Deals: WINDOW_SUM(SUM([Number of Records]))/2

  6. Percent Calc: [Total Cat. Deals]/[Total Deals]

  7. Rank Calc: RANK_UNIQUE([Total Cat. Deals], ‘asc’)

  8. Size Calc: [Percent calc]/WINDOW_MAX([Percent calc])

  9. X: SIN(RADIANS([Index-1])*[Size calc]) * [Rank calc]

  10. Y: COS(RADIANS([Index-1])* [Size calc]) * [Rank calc]

下图展示了如何在 Tableau 中创建计算字段PATH的示例。

作者插图:为 sankey 图创建计算字段的示例,PATH

以下图像展示了如何在PATH上创建一个名为Path (bin) 的 bin,bin 大小为 1。

作者插图:为 sankey 图创建 bin 的示例,路径 (bin)

步骤 3:创建可视化

交易中的鲨鱼数量 移动到Marks card上的Colors tile,将Path(bin) 移动到Rows架。检查Path(bin) 字段上的Show Missing Value

作者插图:检查显示缺失值

Marks card上,将标记类型更改为Line。将Path(Bin)Rows架移到Marks Card上的Path。确保Number of Sharks 是一个维度而不是显示总和。

作者插图:Marks card 上的 PATH(bin) 和交易中的鲨鱼数量

X 移动到Columns架,将Y 移动到Rows架。

作者插图:将XY添加到工作表中

Y上选择编辑表计算

作者插图:在Y上选择编辑表计算

对于Index-1,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:PATH(bin)

作者插图:Index-1 表计算

对于Size calc,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:交易中的鲨鱼数量PATH(bin)

  • 确保交易中的鲨鱼数量在顶部

作者插图:Size calc 表计算

对于Total Cat. Deals,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:PATH(bin)

作者插图:Total Cat. Deals 表计算

对于Total Deals,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:交易中的鲨鱼数量PATH(bin)

  • 确保交易中的鲨鱼数量在顶部

作者插图:Total Deals 表计算

对于Rank calc,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:交易中的鲨鱼数量

作者插图:Rank calc 表计算

X上选择编辑表计算

作者插图:在X上选择编辑表计算

对于Index-1,更新表计算如下:

  • 选择使用特定维度计算

  • 检查以下字段:PATH(bin)

作者图片:索引-1 表计算

对于 大小计算,请按如下方式更新表计算:

  • 选择 按特定维度计算

  • 勾选以下字段:交易中的鲨鱼数量路径(bin)

  • 确保 交易中的鲨鱼数量 位于顶部

作者图片:大小计算 表计算

对于 总类别交易,请按如下方式更新表计算:

  • 选择 按特定维度计算

  • 勾选以下字段:路径(bin)

作者图片:总类别交易 表计算

对于 总交易,请按如下方式更新表计算:

  • 选择 按特定维度计算

  • 勾选以下字段:交易中的鲨鱼数量路径(bin)

  • 确保交易中的鲨鱼数量位于顶部

作者图片:总交易 表计算

对于 排名计算,请按如下方式更新表计算:

  • 选择 按特定维度计算

  • 勾选以下字段:交易中的鲨鱼数量

作者图片:排名计算 表计算

一旦表计算更新,放射状图的基础部分开始成形。

作者图片:放射状图基础

第四步:附加格式设置

使用 标记卡 来格式化视觉效果——调整线条厚度、颜色和标签。下面的图片显示了每条线的开头添加了标签,并分配了新的颜色调色板。还可以使用左侧的 格式面板 来去除边框并调整背景。

作者图片:格式化放射状图

以下图片显示了最终的放射状图。

作者图片:最终的放射状图

嵌套条形图

Tableau 中的另一种高级可视化技术是嵌套条形图。嵌套条形图是一种堆叠条形图的方法,可以让你比较某一类别的两个数值。虽然这种可视化类型没有进入最终仪表板,但以下示例展示了如何使用这些数据创建嵌套条形图。下面的嵌套条形图显示了按行业分类的交易数量和演讲数量。

从创建两个条形图的工作表开始。将 行业 移动到 货架,将 记录数量(即总演讲数)和 获得交易 移动到 货架。

作者图片:显示按行业分类的交易和演讲的两个条形图

获得交易 的下拉选项中,选择 双轴

作者图片:选择双轴

右键点击其中一个 x 轴,选择 同步轴

作者提供的图像:同步图表轴

所有标记卡 上,将类型更改为 条形图

作者提供的图像:将图表类型更改为条形图

使用 记录数量成交情况 标记卡 调整条形的大小。将 成交情况 条形(海军蓝)设为比 记录数量 条形(浅蓝)小,如下图所示。

作者提供的图像:调整条形大小

使用 标记卡 添加标签并根据需要格式化视觉效果。以下图像展示了最终的嵌套条形图。

作者提供的图像:按行业显示总交易和推介的嵌套条形图

这涵盖了 Tableau 中的 6 种高级可视化!虽然这些示例是在 Tableau 中,但请记住,这些可视化也可以在其他工具中创建——所以在开发下一个数据可视化时,使用它们作为灵感吧!

最终的仪表板可以在这里查看或下载源材料。

Payal 是一位数据与人工智能专家。在她的闲暇时间,她喜欢阅读、旅行和在 Medium 上写作。如果你喜欢她的作品, 关注或订阅 她的列表,绝不要错过任何故事!

以上文章为个人观点,不一定代表 IBM 的立场、策略或意见。

参考资料

[1]: Thirumani, Satya. “🦈 Shark Tank Us Dataset 🇺🇸。” Kaggle, 2023 年 8 月 28 日, www.kaggle.com/datasets/thirumani/shark-tank-us-dataset. (CC0: 公共领域许可证)

6 个毁灭你数据科学生产力的坏习惯

原文:towardsdatascience.com/6-bad-habits-killing-your-productivity-in-data-science-ce9c17c7b833

以及如何取得成功

Donato RiccioTowards Data Science Donato Riccio

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

--

图片由作者提供。(AI 生成)

学习数据科学就像学习演奏乐器一样——你必须养成好习惯并打好基础才能成功。

就像音乐家需要练习音阶、琶音和节奏练习才能演奏协奏曲一样,数据科学家需要固化关键实践以开发他们的潜力。

避免有害习惯并培养生产性习惯可以让你将注意力从工作机制转移到工作的艺术性上。

养成使用虚拟环境和跟踪实验等数据科学习惯,可以将你的工作流程从挣扎转变为顺畅的创造过程。

在这篇文章中,我们将深入探讨六个可能会秘密破坏你作为数据科学家效率的日常坏习惯,并提供提升生产力的建议。

使用系统解释器

虚拟环境是与系统环境隔离的 Python 安装,它允许你为特定项目安装包和库而不影响系统的 Python 设置。不使用虚拟环境可能会导致依赖地狱

比如,在我的第一个数据科学项目中,我为图像分类构建了一个机器学习模型。我全局安装了 TensorFlow 2.0 以开始工作。几周后,我的同事给了我一些需要 TensorFlow 1.x 的代码。安装这个版本导致了与我第一个项目依赖的各种冲突!我花了几个小时调试,才意识到应该使用虚拟环境来避免这种混乱。直到我设置了一个匹配我同事原始设置的虚拟环境,我才让继承的代码正常工作。

虚拟环境可以拥有不同的 Python 版本。图片由作者提供(AI 生成元素)

虚拟环境巧妙地避免了这个问题,为每个项目提供了自己的沙盒空间。每个环境都有一个专用的 Python 解释器、pip 和库。

你可以安全地安装库,知道它不会影响其他工作流。不再担心破坏你的 Python 安装!

工具如 Anacondavirtualenv 使得创建和管理虚拟环境变得轻而易举。每个新项目花费 1 分钟激活一个环境,避免了后续数小时的挫折。这是一个能带来巨大时间节省的习惯。

要使用 conda 创建虚拟环境,请使用以下命令。

conda create -n env_name python=3.x

然后,激活环境。

conda activate env_name

这两行代码将为你节省数小时的时间,并让你摆脱依赖地狱。

过度依赖笔记本

Jupyter 笔记本在数据科学中因其直观的工作流和将代码、可视化和文本交织在一起的能力而备受喜爱。

对于协作、可重复性和项目结构来说,将所有工作都放在笔记本中有相当大的缺点。

笔记本难以进行版本控制,且缺乏测试框架和其他环境中存在的 linting 功能。并行编辑很快会导致合并冲突。

也很容易将笔记本用作草稿纸,这会导致文件杂乱无章。

Cookiecutter Data Science. 提供的组织良好的数据科学项目结构

一旦通过了初步探索和原型设计,将你的工作流转移到 .py 脚本和 .py 模块,以便从软件工程最佳实践中受益。

将笔记本代码转移到组织良好的函数和文件中。

使用笔记本来展示发现、建立叙事和分享可重复的结果。

结合笔记本和脚本可以让你兼得两者的优点。

使用鼠标过多

鼠标看起来很直观,但过度点击和菜单搜索浪费了宝贵的时间。键盘提供了更高效的方式来导航和操作你的工作空间。

你可以使用键盘来:

  • 直接跳转到特定的行号,而不是滚动。

  • 选择并同时编辑多行,而不是逐行编辑。

  • 注释和取消注释代码块。

  • 替换多个单词的出现。

  • 直接导航到函数和变量。

  • 立即格式化混乱的代码,而不是手动修复缩进和间距。

  • 通过一步重命名变量和函数来安全地重构代码,跨文件进行操作。

我们的 Llama 正在尝试弄清楚如何使用键盘快捷键。图片由作者提供(AI 生成)

键盘快捷键可以加速你在代码编辑器中进行的几乎所有日常操作。

忘记鼠标,将手放在键盘上,以便更快地编写代码。

跳过数据版本控制

我在构建文本分类模型时,想要尝试不同的预处理方法。我创建了几个变体,但当我找到表现最佳的版本时,我意识到我忘记了使用的确切步骤,因此无法重现结果。

这非常令人沮丧。

代码可以使用 Git 轻松进行版本控制。但是,数据在版本控制最佳实践方面常常被忽视。这在实验和可重复性方面是一个巨大的机会损失。

对数据进行版本控制可以跟踪数据集随时间的变化。你可以自由实验,创建多种变体,以探索数据变化如何改进模型,并且知道如果需要的话,可以快速恢复。

存储元数据(如数据集描述、预处理步骤和预期用途)非常重要,以防数据衰退。记录每个数据版本所代表的内容,以防止可重复性问题。

版本控制让你可以自信地迭代增强数据集,快速探索建模思路,同时知道如果出现问题,你有退路。

我最喜欢的工具是数据版本控制(DVC),因为它易于设置,并且可以连接到不同的云存储服务,包括 Google Drive。

未跟踪实验

当我开始学习数据科学时,我是手动跟踪所有内容的。

从特征到模型架构的每一个细节都散落在不整洁的电子表格中,文件名不一致。

这导致了巨大的混乱,我发现很难重现结果。我浪费了时间手动添加参数,而实验工具可以轻松地自动化这个过程。

引入实验跟踪。机器学习实验涉及许多移动部分——数据样本、特征工程代码、模型配置、性能指标等。一个实验是一组导致特定结果的参数配置,通常在训练运行期间进行跟踪。

MLFlow 中的实验。来源。

MLFlow、Comet 和 W&B 提供了方便的方式来记录你的机器学习工作流中的所有部分——代码版本、数据集、参数和指标。这些系统以有组织、可搜索的结构捕捉端到端的流程。

回顾你的实验跟踪报告,以识别值得追求的成功和从失败中得到的宝贵经验。

基于有效的经验进行构建,而不是每次都重新发明轮子。

未使用代码助手

成为 AI 编码的早期采用者,以最大化你作为数据科学家的生产力。能够快速将想法转化为代码,让你在实现数据科学项目时占据优势。

GitHub Copilot 是一个 AI 工具,它在你编码时在编辑器内建议整行代码和函数。它通过实时建议上下文相关的代码片段来帮助你更快地编写代码。Copilot 通过减少在模板代码和调试上的时间来提高你的生产力。

微软、GitHub 和 MIT 研究人员的近期研究调查了 AI 工具对编程效率的影响。他们设计了一个实验,利用 Copilot 测试有无辅助的开发者在编码任务上的完成时间。

从自由职业平台抽取参与者,他们将 95 名志愿者分为处理组和对照组。处理组进行了 Copilot 演示,而两组都编写了一个 HTTP 服务器。通过测试成功率和完成时间来衡量表现。令人震惊的是,Copilot 组平均完成速度比对照组快超过 55%,仅需 71 分钟对比对照组的 161 分钟!

任务完成时间。Copilot 组(处理组)与对照组。来源

然而,我关于将 AI 工具集成到你的工作流程中的建议不应导致盲目复制粘贴 AI 生成的代码。你不想在不理解你的代码在做什么和为什么的情况下使用它们。

此外,依赖 AI 可能对代码质量产生的影响仍然存在疑问。仔细评估这些工具,以确保它们适当地支持——而非取代——人类的问题解决和创造力。

设置数据科学项目

让我们一起设置一个结构化的项目,以便在 Titanic 数据集上进行二分类项目,使用 MLFlow 和 DVC。

首先,创建一个项目和 Git 仓库:

mkdir titanic-project  
cd titanic-project
git init

创建一个 Conda 环境并安装一些库。

conda create -n titanic python=3.11
conda activate titanic
pip install pandas sklearn jupyter mlflow

组织项目结构:

mkdir data models notebooks figures src
cd data
mkdir raw processed
cd ../src 
mkdir features models visualization

src/models/train_model.py中,使用 MLflow 记录参数和指标。这是一个非常简化的示例,应该能给你一些启示。它跟踪一个具有n_estimators和交叉验证准确率的随机森林模型。

import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold

def train_random_forest(n_estimators):
    # Load data
    ...

    # Model training
    rf = RandomForestClassifier(n_estimators=n_estimators)
    scores = cross_val_score(rf, X, y, cv=StratifiedKFold(5))

    return rf, scores

if __name__ == "__main__":
    n_estimators = 100
    with mlflow.start_run():
        rf, scores = train_random_forest(n_estimators)

        # Log parameter, metrics, and model to MLflow
        mlflow.log_param("n_estimators", n_estimators)
        mlflow.log_metric("cv_accuracy", scores.mean())
        mlflow.sklearn.log_model(rf, "model")

src/features/build_features.py中,使用 DVC 来版本控制数据:

# Feature Engineering function
def feature_engineering(df):
    # Placeholder function, add your feature engineering steps here
    return df

# Apply feature engineering
titanic_transformed = feature_engineering(titanic)

# Save transformed dataset
titanic_transformed.to_csv('../../data/processed/titanic_transformed.csv', index=False)

# Track with DVC
os.system("dvc add titanic.csv")
os.system("dvc add titanic_transformed.csv")
os.system("dvc commit -m 'Ran feature engineering'")

现在你的数据版本与模型参数和指标一起进行跟踪。每次更改内容时,请记得提交到 git 和 DVC。

模块化代码、MLflow 和 DVC 提供了端到端的实验和可重复性。版本控制数据和模型允许你迭代地改进工作流程。

一旦整理好,你应该会得到如下效果:

├── data
│   ├── raw
│   └── processed
├── models
├── notebooks
│   ├── eda.ipynb
├── figures
└── src
    ├── features
    │   └── build_features.py
    ├── models
    │   ├── train_model.py
    │   └── predict_model.py
    └── visualization
        └── visualize.py

源代码和输出被分开,以便于跟踪和版本控制。

这只是一个概念,你应该根据自己的需求进行自定义。

生产力的新高度

数据科学的生产力不仅仅依赖于技术知识,还依赖于养成良好的习惯。避免常见的陷阱,如忽视虚拟环境、过度使用笔记本以及未能跟踪实验。相反,应培养以下习惯:利用键盘快捷键、对数据进行版本控制、系统地跟踪实验,并将重复性工作交给人工智能。

喜欢这篇文章吗?通过订阅我的新闻通讯,每周将数据科学面试问题送到你的收件箱, 数据面试

此外,你也可以在 LinkedIn上找到我。

6 个你应该了解的与索引相关的 Pandas 操作

原文:towardsdatascience.com/6-common-index-related-operations-you-should-know-about-pandas-783fdba59768

在数据框中有效地处理索引

Yong CuiTowards Data Science Yong Cui

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 10 月 17 日

--

图片由 Alejandro Luengo 提供,来源于 Unsplash

想象一下,你有一个装满成千上万本书的图书馆,每本书都包含大量信息。为了找到你需要的确切书籍,你会查看图书馆的索引(如果有的话),对吧?在处理实际数据时,拥有一个类似图书馆的索引对于你筛选大量数据、准确找到所需信息而无需翻遍每一部分是至关重要的。

在这篇文章中,我将分享一些常见但重要的与索引相关的操作,通过简单的适用场景来拆解这些操作。无论你是数据新手还是经验丰富的专业人士,你将很快看到这些操作如何成为你数据的最佳伙伴。

不再赘述,开始吧。

简单说明一下,在数据框中,行和列都被视为索引,但在大多数数据操作中,我们通常只将行视为感兴趣的索引,因为许多数据集以宽格式呈现——每一行代表一个数据记录,每一列代表数据记录的不同方面。

在这篇文章中,我们将重点处理沿行操作索引。也就是说,每个索引项对应一行。

1. 设置索引

常见的数据操作从导入数据开始,比如 CSV 文件。下面的截图展示了来自 GitHub 的在线数据源。

本博客文章使用的源数据(由作者提供的截图)

当你导入这个文件时,你会看到一个“额外”的没有名称的列出现在我们在源文件中看到的三个列之前。

# Read raw data online
import pandas as pd

data_url = "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/flights.csv"
df = pd.read_csv(data_url)
df

从源读取的数据框显示索引

自动生成的索引从 0 开始,并为每个附加行递增 1。你可以通过调用以下方法访问索引:

>>> df.index
RangeIndex(start=0, stop=144, step=1)

如果你在之前知道数据结构,在本例中,数据集具有年和月作为数据的唯一标识符。你可以在读取数据时将这两列设置为索引,如下所示:

>>> df = pd.read_csv(data_url, index_col=["year", "month"])
>>> df
                passengers
year month                
1949 January           112
     February          118
     March             132
     April             129
     May               121
...                    ...
1960 August            606
     September         508
     October           461
     November          390
     December          432

[144 rows x 1 columns]

请注意,由于我们将两个列设置为索引,我们正在创建一个使用多重索引的数据框:

>>> df.index
MultiIndex([(1949,   'January'),
            (1949,  'February'),
            (1949,     'March'),
            (1949,     'April'),
            (1949,       'May'),
            (1949,      'June'),
            (1949,      'July'),
            (1949,    'August'),
            (1949, 'September'),
            (1949,   'October'),
            ...
            (1960,     'March'),
            (1960,     'April'),
            (1960,       'May'),
            (1960,      'June'),
            (1960,      'July'),
            (1960,    'August'),
            (1960, 'September'),
            (1960,   'October'),
            (1960,  'November'),
            (1960,  'December')],
           names=['year', 'month'], length=144)

由于多重索引不是本文的重点,我不会在这里过多讨论。然而,如果你对多重索引感兴趣,请参考我之前的文章

2. 重置索引

让我们继续讨论具有多重索引的数据框。因为我们中的许多人不太习惯多重索引,我们仍然希望使用单一标量索引。为此,我们可以重置索引,如下所示:

>>> df = df.reset_index()
>>> df
     year      month  passengers
0    1949    January         112
1    1949   February         118
2    1949      March         132
3    1949      April         129
4    1949        May         121
..    ...        ...         ...
139  1960     August         606
140  1960  September         508
141  1960    October         461
142  1960   November         390
143  1960   December         432

[144 rows x 3 columns]

从输出中,你可以看出我们现在使用的是基于 0 的索引。对于reset_index函数,它有一个参数drop,默认设置为False,以通过将原始索引转换为列来保留原始索引。然而,有时你可能希望丢弃原始索引,例如在合并/组合数据框时。

>>> df1 = df.copy()
>>> combined_df = pd.concat([df, df1])
>>> combined_df.index
Index([  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,
       ...
       134, 135, 136, 137, 138, 139, 140, 141, 142, 143],
      dtype='int64', length=288) 

如上所示的简单示例,合并的数据框具有由两个数据框的索引组成的索引(顺便提一下,Pandas 允许索引中有重复值)。在大多数情况下,我们不希望索引中有重复项,因此我们希望重置索引,如下所示。

>>> combined_df.reset_index(drop=False)
     index  year      month  passengers
0        0  1949    January         112
1        1  1949   February         118
2        2  1949      March         132
3        3  1949      April         129
4        4  1949        May         121
..     ...   ...        ...         ...
283    139  1960     August         606
284    140  1960  September         508
285    141  1960    October         461
286    142  1960   November         390
287    143  1960   December         432

[288 rows x 4 columns]
>>> combined_df.reset_index(drop=True)
     year      month  passengers
0    1949    January         112
1    1949   February         118
2    1949      March         132
3    1949      April         129
4    1949        May         121
..    ...        ...         ...
283  1960     August         606
284  1960  September         508
285  1960    October         461
286  1960   November         390
287  1960   December         432

[288 rows x 3 columns]

在上述代码片段中,我展示了当drop设置为False(默认值)和True时的对比。正如你所见,在这种情况下,理想的操作是后者——丢弃原始索引。

3. 使用索引选择数据

我们已经学习了如何设置和重置索引,但我们可以用索引做些什么呢?正如你可能已经注意到的,索引标记了数据的位置,作为每行的标识符,因此第一个用途是使用索引进行数据选择。

在使用索引进行数据选择时,最常用的方法是使用iloc方法。以下代码演示了如何选择单行数据。

>>> df.iloc[0]
year             1949
month         January
passengers        112
Name: 0, dtype: object
>>> df.iloc[1, :]
year              1949
month         February
passengers         118
Name: 1, dtype: object

如上所示,你可以使用一个数字——在本例中,0 选择了第一行,或者你可以使用一个数字和: (选择所有列)。如果你想选择一个或一个子集的列,你需要指定列的索引:

>>> df.iloc[2, 0]
1949
>>> df.iloc[2, :2]
year      1949
month    March
Name: 2, dtype: object

在上述示例中,iloc[2, 0]选择了第 3 行第 1 列的数据。请注意,行和列都使用基于 0 的索引。代码iloc[2, :2]选择第 3 行和前两个列(第 0 列和第 1 列)—就像其他 Python 切片操作一样,结束索引(在本例中为 2)是不包括在内的。

另一个需要了解的事情是,当你有许多列时,通过计数来找出感兴趣列的索引可能会很繁琐。幸运的是,我们可以使用以下解决方案:

>>> df.iloc[2:5, df.columns.get_loc("month")]
2    March
3    April
4      May
Name: month, dtype: object

4. 使用索引合并数据

当你处理多个数据框时,你通常需要合并数据以进行后续处理。虽然你可以使用各种方法进行合并操作,如mergejoincombineconcat,但最通用的方法是merge

在我的日常数据处理工作中,我大多使用命名列来进行merge。以下是一个代码示例。我们通过将现有年份加 11 来创建了一些模拟数据,并使用了“year”“month”进行合并。

>>> df2 = df.copy()
>>> df2["year"] = df2["year"] + 11
>>> df.merge(df2, on=["year", "month"])
    year      month  passengers_x  passengers_y
0   1960    January           417           112
1   1960   February           391           118
2   1960      March           419           132
3   1960      April           461           129
4   1960        May           472           121
5   1960       June           535           135
6   1960       July           622           148
7   1960     August           606           148
8   1960  September           508           136
9   1960    October           461           119
10  1960   November           390           104
11  1960   December           432           118

但是,merge方法也支持基于索引的合并。以下是一个示例。

>>> df.merge(df2, left_index=True, right_index=True)
     year_x    month_x  passengers_x  year_y    month_y  passengers_y
0      1949    January           112    1960    January           112
1      1949   February           118    1960   February           118
2      1949      March           132    1960      March           132
3      1949      April           129    1960      April           129
4      1949        May           121    1960        May           121
..      ...        ...           ...     ...        ...           ...
139    1960     August           606    1971     August           606
140    1960  September           508    1971  September           508
141    1960    October           461    1971    October           461
142    1960   November           390    1971   November           390
143    1960   December           432    1971   December           432

[144 rows x 6 columns]

如上所示,我们通过将True设置为left_indexright_index来指定使用索引。这种方式,我们得到一个合并后的数据框,其索引与单个数据框相同。

5. 导出数据时包括/排除索引

当我们与合作伙伴一起工作时,我们需要共享数据集。数据的一种常见格式是 CSV 格式。因此,我们需要将数据框导出为 CSV 文件,如下所示:

>>> df.to_csv("exported_df.csv")
>>> with open("exported_df.csv") as file:
...     some_data = file.read(110)
... 
>>> print(some_data)
,year,month,passengers
0,1949,January,112
1,1949,February,118
2,1949,March,132
3,1949,April,129
4,1949,May,121

如上面的代码片段所示,我们使用to_csv方法创建一个 CSV 文件。你可能会注意到,当我们读取 CSV 文件时,我们发现 CSV 文件中有一个额外的列,除了我们开始时的 3 列。很容易看出,额外的列就是索引。也就是说,当你将数据框导出到 CSV 文件时,默认情况下会导出索引。然而,如果包含这个索引列可能会让其他人感到困惑,因此你可以通过以下方法覆盖默认行为:

>>> df.to_csv("exported_df_no_index.csv", index=False)
>>> with open("exported_df_no_index.csv") as file:
...     other_data = file.read(110)
... 
>>> print(other_data)
year,month,passengers
1949,January,112
1949,February,118
1949,March,132
1949,April,129
1949,May,121
1949,June,

从打印输出中,你可以看出索引已经从导出的 CSV 文件中移除。这是因为我们在to_csv方法调用中将索引参数设置为 False。此设置指示不要在导出的文件中包含索引。

6. 在 Groupby 操作中将索引转换为列

当你汇总数据时,一种常见的方法是使用groupby方法。例如,在数据框中,我们想要找出每年的乘客总数,如下所示:

>>> total_passengers = df.groupby("year")["passengers"].sum()
>>> total_passengers
year
1949    1520
1950    1676
1951    2042
1952    2364
1953    2700
1954    2867
1955    3408
1956    3939
1957    4421
1958    4572
1959    5140
1960    5714
Name: passengers, dtype: int64

如上所示,我们通过按年份分组数据来计算乘客总数。你可能没有注意到的一点是,输出的total_passengersSeries格式。

>>> type(total_passengers)
<class 'pandas.core.series.Series'>

如你所知,Series与其他数据框一起使用时并不是最佳选择。因此,如果我们能在 Groupby 操作中获得数据框作为输出,通常会更为理想。以下展示了如何操作:

>>> total_passengers1 = df.groupby("year", as_index=False)["passengers"].sum()
>>> total_passengers1
    year  passengers
0   1949        1520
1   1950        1676
2   1951        2042
3   1952        2364
4   1953        2700
5   1954        2867
6   1955        3408
7   1956        3939
8   1957        4421
9   1958        4572
10  1959        5140
11  1960        5714
>>> type(total_passengers1)
<class 'pandas.core.frame.DataFrame'>

groupby方法调用中,我们只是简单地将as_index设置为False,这意味着我们不希望使用分组变量作为结果数据框。使用这种数据框输出,操作会更简单。

结论

在本文中,我们回顾了与数据框中的索引相关的最常见的 6 个操作。希望你觉得这篇文章有用。

想了解更多关于 Python 和 Pandas 的内容吗?请考虑订阅我的电子邮件通讯。如果你想系统地提升你的 Python 技能,请查看 Manning 出版的我的新书 Python How-To

数据科学代码中需避免的 6 个常见错误

原文:towardsdatascience.com/6-common-mistakes-to-avoid-in-data-science-code-d0fc38f96594

以及如何克服这些问题

Khuyen TranTowards Data Science Khuyen Tran

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

--

动机

数据科学家通常在迭代和探索性环境中工作。因此,常常关注快速结果而非创建可维护或可扩展的代码。

然而,数据科学家必须避免编写糟糕的代码,原因如下:

  1. 减少代码可读性: 编写不佳的代码可能难以阅读和理解,使得原作者和其他团队成员在未来维护或修改代码时变得更加困难。

  2. 增加引入错误的机会: 结构不良或效率低下的代码更容易出错,可能影响分析或模型的准确性。

  3. 集成挑战: 编写不佳的代码可能会阻碍与生产系统的集成,以及向其他团队成员(包括数据工程师和机器学习工程师)的交接。

在数据科学项目中编写更好的代码至关重要,需要认识和解决常见的糟糕实践,包括:

  1. 过度使用 Jupyter Notebooks

  2. 模糊的变量名

  3. 冗余代码

  4. 重复的代码段

  5. 频繁使用全局变量

  6. 缺乏适当的代码测试

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

这些糟糕的实践使得代码的可读性、可重用性和可维护性降低。

为了说明这些问题,我们将研究在 Kaggle 竞赛中参与的我如何在 Kaggle 竞赛中进入前 0.3%笔记本,该竞赛是房价 — 高级回归技术

我选择了这个笔记本,因为它展示了与我合作过的数据科学家的代码中常见错误相似的编码实践。通过检查这个笔记本,我们可以获得有关数据科学家应避免的陷阱的宝贵见解。

Jupyter 笔记本的过度使用

问题

Jupyter 笔记本提供了一个交互式的代码执行、可视化和即时反馈环境,使其在探索性分析和概念验证中非常有价值。

然而,由于多种原因,数据科学家不适合将 Jupyter 笔记本用于生产相关任务,例如特征工程和模型训练。

单元格执行中的依赖性问题

首先,一些单元格可能依赖于之前单元格的输出,以不同的顺序执行可能会导致依赖单元格中的错误或不一致。

在提供的示例中,在执行第 16 个单元格之前执行第 18 个单元格会导致两个异常值被移除。

丢弃的行数:2

然而,在第 16 个单元格之前执行第 18 个单元格会导致三个异常值被移除。

丢弃的行数:3

性能问题

其次,笔记本经常包含可视化和分析代码以及生产代码。这种将代码混合在一个笔记本中的做法可能会导致资源密集型任务,从而对生产系统的性能产生负面影响。

解决方案

用笔记本进行 EDA 和分析,同时使用 Python 脚本进行特征工程和机器学习模型训练。

为了进一步组织你的项目,在特征工程之前创建一个数据分析的笔记本,而在特征工程之后创建另一个分析中间数据的笔记本。

.
├── data/
│   ├── raw
│   ├── intermediate
│   └── final
├── notebooks/
│   ├── pre_processing.ipynb
│   └── post_processing.ipynb
└── src/
    ├── __init__.py
    ├── process_data.py
    └── train_model.py

这种方法可以在各种项目中使用 Python 脚本,同时保持笔记本的清洁和有序。

模糊的变量名

问题

在以下代码片段中,变量reslslm的含义不明确,使得审阅者难以理解代码的逻辑,并可能导致变量的误用。

解决方案

使用描述性和有意义的变量名,以传达变量的目的和内容。

def add_log_transform_columns(data, columns):
    num_columns = data.shape[1]
    for column in columns:
        transformed_column = pd.Series(np.log(1.01 + data[column])).values
        data = data.assign(new_column=transformed_column)
        data.columns.values[num_columns] = column + '_log'
        num_columns += 1
    return data

冗余代码

问题

降低代码可读性

冗余代码会降低代码的可读性。

在笔记本中,YrSold列在整数和字符串类型之间进行不必要的转换。

最初,YrSold列被表示为整数:

随后,代码将YrSold列转换为字符串:

然后,YrSold 列暂时被转换回整数:

最后,YrSold 列以及其他分类列使用独热编码转换回整数:

all_features.filter(regex="YrSold").dtypes
"""
YrSold_2006    int64
YrSold_2007    int64
YrSold_2008    int64
YrSold_2009    int64
YrSold_2010    int64
"""

这些不必要的转换会使得作者和维护者难以跟踪列的数据类型,从而可能导致列的错误使用。

负面性能影响

多余的代码也会通过引入不必要的计算开销影响性能。

在提供的代码中,作者不必要地使用 pd.DataFrame(df) 两次来创建两个 DataFrame 的副本。然而,创建 DataFrame 的副本是不必要的,因为目标只是为了获取列名。

如果原始 DataFrame 很大,创建新的 DataFrame 可能会计算开销很大。

解决方案

保持你的代码简洁明了。删除那些对程序没有价值的多余代码行。

例如,我们可以将上述代码重写为直接从原始 DataFrame 中获取列。

import pandas as pd

def percent_missing(df):
    columns = list(df)
    dict_x = {}
    for i in range(0, len(columns)):
        dict_x.update({columns[i]: round(df_copy[columns[i]].isnull().mean() * 100, 2)})
    return missing_percentages

代码段重复

问题

代码重复增加了维护的负担。

代码 1 if x > 0 else 0 被多次重用。任何修改或更新,比如将其更改为 1 if x < 0 else 0,都需要在每个重复代码实例中进行相同的更改。这一过程既耗时又容易出错。

解决方案

将重复的代码封装在函数或类中,以提高代码的重用性和可维护性。

例如,我们可以创建一个名为 is_positive 的函数,将代码片段 1 if x > 0 else 0 封装在其中。

def is_positive(column):
  return 1 if column > 0 else 0

all_features['haspool'] = all_features['PoolArea'].apply(is_positive)
all_features['has2ndfloor'] = all_features['2ndFlrSF'].apply(is_positive)
all_features['hasgarage'] = all_features['GarageArea'].apply(is_positive)
all_features['hasbsmt'] = all_features['TotalBsmtSF'].apply(is_positive)
all_features['hasfireplace'] = all_features['Fireplaces'].apply(is_positive)

## Python 清理代码:使你的 Python 函数更具可读性的 6 个最佳实践

停止编写需要超过 3 分钟才能理解的 Python 函数

towardsdatascience.com

频繁使用全局变量

问题

使用全局变量可能会导致混淆,并且很难理解值是如何以及在哪里被修改的。

在以下代码中,Xtrain_labelskf 是在代码库的不同部分定义的全局变量。

在查看函数调用时,维护者可能错误地认为cv_rmse函数只需要定义model变量即可调用:

model = LinearRegression()

rmse_scores = cv_rsme(model)

,… 但实际上,该函数还需要定义Xtrain_labelskf

Traceback (most recent call last):
  File "/Users/khuyentran/software-engineering-for-data-scientists/variables/global_variables/main.py", line 20, in <module>
    scores = cv_rsme(model)
             ^^^^^^^^^^^^^^
  File "/Users/khuyentran/software-engineering-for-data-scientists/variables/global_variables/main.py", line 13, in cv_rsme
    return np.sqrt(-cross_val_score(model, X, train_labels, scoring='neg_mean_squared_error', cv=kf))
                                           ^
NameError: name 'X' is not defined

解决方案

不要使用全局变量,而是将必要的变量作为参数传递给函数。这将使函数更模块化,更易于测试。

def cv_rmse(model, X, train_labels, kf):
    rmse = np.sqrt(-cross_val_score(model, X, train_labels, scoring="neg_mean_squared_error", cv=kf))
    return rmse

model = LinearRegression()
rmse_scores = cv_rmse(model, X=..., train_labels=..., kf=...)
print((rmse_scores.mean(), rmse_scores.std()))
# (1.092857142857143, 0.5118992247762547)

缺乏适当的代码测试

问题

隐藏的代码问题

未经测试的代码可能会产生意外的结果,即使输出看似正确。

在代码示例中,使用create_booleans函数处理整数应该将其转换为 0 和 1。输出看似正确,但实际上是错误的。非零值应该是 1,而零值应该是 0。

import pandas as pd

data = {
    'WoodDeckSF': [150, 0, 80, 120, 200],
    'OpenPorchSF': [30, 40, 0, 20, 60],
    'EnclosedPorch': [0, 20, 10, 0, 30],
    '3SsnPorch': [0, 0, 0, 15, 0],
    'ScreenPorch': [0, 0, 25, 0, 40]
}

all_features = pd.DataFrame(data)

all_features['HasWoodDeck'] = (all_features['WoodDeckSF'] == 0) * 1
all_features['HasOpenPorch'] = (all_features['OpenPorchSF'] == 0) * 1
all_features['HasEnclosedPorch'] = (all_features['EnclosedPorch'] == 0) * 1
all_features['Has3SsnPorch'] = (all_features['3SsnPorch'] == 0) * 1
all_features['HasScreenPorch'] = (all_features['ScreenPorch'] == 0) * 1
all_features.iloc[:, -5:]

# The results are wrong
"""
   HasWoodDeck  HasOpenPorch  HasEnclosedPorch  Has3SsnPorch  HasScreenPorch
0            0             0                 1             1               1
1            1             0                 0             1               1
2            0             1                 0             1               0
3            0             0                 1             0               1
4            0             0                 0             1               0
"""

依赖不准确的结果可能导致错误的分析和误导性的结论。

解决方案

通过单元测试,我们可以指定预期的输出,减少遗漏错误的可能性。

import pandas as pd
from pandas.testing import assert_series_equal

def create_booleans(feature):
    return (feature == 0) * 1

def test_create_booleans():
    feature = pd.Series([4, 2, 0, 1])
    expected = pd.Series([1, 1, 0, 1])
    actual = create_booleans(feature)
    assert_series_equal(expected, actual)
============================ FAILURES ============================
______________________ test_create_booleans ______________________
    def test_create_booleans():
        feature = pd.Series([4, 2, 0, 1])
        expected = pd.Series([1, 1, 0, 1])
        actual = create_booleans(feature)
>       assert_series_equal(expected, actual)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

E   AssertionError: Series are different
E   
E   Series values are different (100.0 %)
E   [index]: [0, 1, 2, 3]
E   [left]:  [1, 1, 0, 1]
E   [right]: [0, 0, 1, 0]
E   At positional index 0, first diff: 1 != 0

被忽视的边缘情况

问题

代码在特定条件下可能表现良好,但在其他情况下可能出现意外行为。

在这个示例中,代码根据MSSubClass列中每组值的众数填充MSZoning列中的缺失值。当MSSubClass没有 NaN 值时,它按预期工作。

features = pd.DataFrame(
 {
  "MSZoning": [1, np.nan, 2, 3, 4, 5, 6, np.nan],
    "MSSubClass": ["a", "a", "a", "a", "b", "b", "b", "b"],
  }
)

features['MSZoning'] = features.groupby('MSSubClass')['MSZoning'].transform(lambda x: x.fillna(x.mode()[0]))
list(features["MSZoning"])
# [1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 4.0]

然而,当MSSubClass包含缺失值时,代码的表现不如预期,留下了MSZoning中的一些缺失值。

features = pd.DataFrame(
    {
        "MSZoning": [1, np.nan, 2, 3, 4, 5, 6, np.nan],
        "MSSubClass": ["a", "a", np.nan, "a", np.nan, "b", "b", "b"],
    }
)

features['MSZoning'] = features.groupby('MSSubClass')['MSZoning'].transform(lambda x: x.fillna(x.mode()[0]))
list(features["MSZoning"])
# [1.0, 1.0, nan, 3.0, nan, 5.0, 6.0, 5.0]

忽视边缘情况可能导致现实应用中的问题。

解决方案

我们可以使用单元测试来测试边缘情况:

 def fill_missing_values_with_mode_in_a_group(
    df: pd.DataFrame, group_column: str, target_column: str
) -> pd.DataFrame:
    df[target_column] = df.groupby(group_column)[target_column].transform(
        lambda x: x.fillna(x.mode()[0])
    )
    return df

def test_fill_missing_values_with_mode_in_a_group():
    data = pd.DataFrame(
        {
            "col1": [1, np.nan, 2, 3, 4, 5, 6, np.nan],
            "col2": ["a", "a", np.nan, "a", np.nan, "b", "b", "b"],
        }
    )
    imputed_data = fill_missing_values_with_mode_in_a_group(
    df=data, 
    group_column="col2", 
    target_column="col1",
    )
    assert imputed_data['col1'].isnull().sum() == 0, "There are missing values in the column."AssertionError: There are missing values in the column.
assert 2 == 0
AssertionError: There are missing values in the column.
assert 2 == 0

… 并调整代码以考虑边缘情况:

 def fill_missing_values_with_mode_in_a_group(
    df: pd.DataFrame, group_column: str, target_column: str
) -> pd.DataFrame:
  if df[group_column].isna().any():
    raise ValueError(
          f"The {group_column} used for grouping cannot contain null values"
    )
    df[target_column] = df.groupby(group_column)[target_column].transform(
        lambda x: x.fillna(x.mode()[0])
    )
    return df

def test_fill_missing_values_with_mode_in_a_group():
  with pytest.raises(ValueError):
        data = pd.DataFrame(
            {
                "col1": [1, np.nan, 2, 3, 4, 5, 6, np.nan],
                "col2": ["a", "a", np.nan, "a", np.nan, "b", "b", "b"],
            }
        )
        imputed_data = fill_missing_values_with_mode_in_a_group(
      df=data, 
      group_column="col2", 
      target_column="col1",
      )

结论

本文讨论了数据科学项目中遇到的常见挑战,并提供了一些实用的解决方案。请注意,本文并未穷尽所有可能的解决方案,而是提供了一些可能帮助克服这些问题的策略。

要获取有关在数据科学项目中集成最佳实践的全面指南,请参考以下文章:

## 如何结构化数据科学项目以提高可读性和透明度

如何在一行代码中创建一个

towardsdatascience.com ## 在数据科学项目中停止硬编码 — 改用配置文件

如何高效地与 Python 中的配置文件交互

[towardsdatascience.com ## 数据科学家的 Git 深度解析

通过真实场景学习 Git

[towardsdatascience.com ## 数据科学家的 Pytest

针对你的数据科学项目的 Pytest 综合指南

[towardsdatascience.com

你可能会犯的 6 个尴尬的 Sklearn 错误及如何避免它们

原文:towardsdatascience.com/6-embarrassing-sklearn-mistakes-you-may-be-making-and-how-to-avoid-them-6be5bbdbb987

没有错误信息——这就是它们微妙的原因

Bex T.Towards Data Science Bex T.

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

--

通过 Sklearn 学习避免初学者常犯的六个与机器学习理论相关的严重错误。

图片由我与 Leonardo AI 制作

通常,Sklearn 在你犯错时会抛出大红色的错误信息和警告。这些信息表明你的代码有严重问题,阻止了 Sklearn 魔法的正常运行。

但如果你没有收到任何错误或警告会发生什么?这是否意味着你做得很好?不一定。许多旋钮和拨盘使 Sklearn 成为最优秀的 ML 库,其世界级的代码设计就是一个例子。

编写 Sklearn 代码时的错误很容易修复。可以被忽视的是与内部逻辑和支持 Sklearn 算法及变换器的 ML 理论相关的错误。

这些错误在你是初学者时尤其常见且微妙。因此,本文将讲述我在初学者阶段犯的六个错误以及如何避免它们。

1️⃣. 到处使用 fitfit_transform

让我们从最严重的错误开始——这是与数据泄漏相关的错误。数据泄漏很微妙,可能对模型性能造成破坏。

当在模型训练过程中使用预测时无法获得的信息时,就会发生数据泄漏。数据泄漏会导致模型给出非常乐观的结果,即使在交叉验证中也是如此,但在测试实际的新数据时表现会非常糟糕。

数据泄漏在数据预处理中很常见,特别是当训练集和测试集没有分开时。许多 Sklearn 预处理变换器,如 插补器,归一化器、标准化函数和对数变换器,在拟合期间会接触到底层的数据分布。

例如,StandardScaler通过从每个样本中减去均值并除以标准差来归一化数据。对完整数据(X)调用fit()函数允许变换器学习每个特征的整个分布的均值和标准差。

如果这些数据在变换之后被拆分为训练集和测试集,那么训练集将会被污染,因为StandardScaler泄露了实际分布中的重要信息。

尽管这对我们可能不明显,但 Sklearn 算法足够强大,能够察觉到这一点并在测试中利用它。换句话说,训练数据对模型来说可能过于完美,因为它包含了关于测试集的有用信息,而测试集对于模型在实际未见数据上的表现测试可能不够新颖。

最简单的解决方案是绝不要在全部数据上调用fit。在进行任何预处理之前,始终将数据拆分为训练集和测试集。即使在拆分之后,你也绝不要在测试集上调用fitfit_transform,否则你将遇到相同的问题。

由于训练集和测试集都应接受相同的预处理步骤,一个黄金法则是对训练数据使用fit_transform——这确保了变换器仅从训练集中学习并同时进行转换。然后,在测试集上调用transform方法,根据从训练数据中学到的信息进行转换。

更稳健的解决方案是使用 Sklearn 的内置管道。管道类专门用于保护算法免受数据泄漏的影响。使用管道可以确保训练数据仅在fit期间使用,而测试数据仅用于计算。你可以在我的另一篇文章中详细了解它们:

## 如何使用 Sklearn 管道编写极简代码

编辑描述

[towardsdatascience.com

2️⃣. 仅通过测试分数评估模型性能

你获得了超过 0.85 的测试分数——这值得庆祝吗?大大的“不”!

尽管高测试得分通常意味着鲁棒的表现,但解读测试结果时有一些重要的警告。首先也是最重要的,无论得分值如何,测试得分应仅根据你从训练中得到的分数来判断。

你唯一应该对你的模型感到满意的时候,是当训练得分高于测试得分,并且两者都足够高以满足你独特情况的期望。然而,这并不意味着训练得分和测试得分之间的差异越大越好。

例如,0.85 的训练得分和 0.8 的测试得分表明模型既不过拟合也不欠拟合。但如果训练得分超过 0.9 而测试得分为 0.8,你的模型就是过拟合了。模型在训练过程中没有进行泛化,而是记住了部分训练数据,导致测试得分大幅下降。

你常常会在基于树的和集成模型中看到这种情况。例如,像随机森林这样的算法如果不控制树的深度,往往会获得非常高的训练得分,导致过拟合。你可以阅读这个讨论来了解训练和测试得分之间的差异。

还有一种情况是测试得分高于训练得分。如果测试得分即使略微高于训练得分,也要感到警惕,因为你犯了错误!这种情况的主要原因是数据泄露,我们在上一节中讨论了一个例子。

有时候,即使在训练集上取得了良好的评分,测试集上的评分也可能极低。当训练和测试得分之间的差异很大时,这个问题往往与测试集有关,而不是过拟合。这可能是由于对训练集和测试集使用了不同的预处理步骤,或者仅仅是忘记对测试集进行预处理所导致的。

总之,始终仔细检查训练和测试得分之间的差距。这将告诉你是否应该应用正则化以克服过拟合,寻找在预处理中可能犯的错误,或者在最佳情况下,为最终评估和部署准备模型。

3️⃣. 在分类中生成不正确的训练/测试集

初学者常见的一个错误是忘记为分类任务生成分层的训练集和测试集。

当新数据分布尽可能匹配训练数据时,模型更可能产生正确的预测。在分类问题中,我们只关心类别权重或比例。

例如,在一个三类分类问题中,类别权重为 0.4、0.3、0.3。当我们将数据划分为训练集和测试集时,这两个集合的分布应该反映完整数据的分布。

我们通常使用 Sklearn 的train_test_split函数来划分数据,Sklearn 提供了一个方便的参数 - stratify,用于生成 stratified splits。以下是有和没有分层切分的训练/测试集的示例:

查看分裂前的类别权重

在没有分层的情况下再次检查训练和测试集中的类别权重。

如你所见,训练集和测试集在第一类和第二类上有不同的类别权重。让我们解决这个问题:

在分层切分后检查训练/测试集的类别权重。

stratify设置为目标(y)在训练集和测试集中产生了相同的类别权重。

类别权重的变化是一个严重的问题,可能使模型对某一特定类别更有偏见。忘记生成分层切分可能导致训练集或测试集变得更加有利,或者引发如下问题:

图片来自作者

上面是我在开始学习 Sklearn 时构建的 KNN 分类器的表现。如你所见,几乎所有测试分数都高于训练分数,因为我忘记生成分层切分。因此,测试集的分布对我的模型来说过于有利。

解决问题后:

图片来自作者

一切恢复正常。

使用交叉验证或管道时,你不必担心这个问题,因为 CV 分割器在底层使用[StratifiedKFold](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedKFold.html)进行分类问题的分层。

4️⃣. 使用LabelEncoder对 X 数组进行编码

你是否曾经因为发现[LabelEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.LabelEncoder.html)只能一次编码一个类别列而感到烦恼?与可以同时转换多个特征的其他文本转换器,如[OneHotEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html)相比,这似乎有点让人失望😔。

但我要告诉你,实际上并不是这样!这只是我们不愿意阅读文档的结果。以下是LabelEncoder的两句文档摘录:

这个转换器应该用于编码目标值,即*y*,而不是输入*X*

那么,我们用什么来编码有序文本特征呢?如果我们亲切地查阅 Sklearn 用户指南中的编码分类特征,我们会看到它清楚地说明:

为了将类别特征转换为整数代码,我们可以使用*OrdinalEncoder*。这个估计器将每个类别特征转换为一个新的整数特征(0 到 n_categories - 1)

使用[OrdinalEncoder](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OrdinalEncoder.html)可以让我们一次性转换多个列,并且它具有能够集成到 Pipeline 实例中的优点,而LabelEncoder则不能。该编码器遵循 Sklearn 的熟悉转换器 API:

你可以通过阅读文档和用户指南来了解很多关于 Sklearn 的内容!

5️⃣. 在没有交叉验证的情况下判断模型性能

我认为你已经对过拟合这一主题感到舒适。过拟合在机器学习中是一个非常紧迫的问题,因此已经设计了无数种技术来克服它。

最基本的方式是将一部分数据保留作为测试集,以模拟和测量模型在未见数据上的表现。然而,模型的超参数可以被调整,直到模型在特定测试集上达到最高分,这又意味着过拟合。

我们可能会将另一部分完整数据作为‘验证集’再进行一次处理。一个模型会在训练数据上进行训练,在验证集上微调其性能,并通过测试集进行最终评估。

但将我们宝贵的数据划分为 3 个集合将意味着模型可以学习的数据量减少。模型的整体表现将取决于那一对特定的训练集和验证集。

所以,机器学习从业者通常使用一种叫做 K 折交叉验证(简称 CV)的程序。根据其值,完整数据被分为K个称为折叠的集合,对于每个折叠,模型将使用K-1个折叠作为训练数据,其余作为测试集。完成 CV 后,模型将在所有数据上进行训练和测试。以下是 Sklearn 为 5 折 CV 绘制的过程图:

图片来源于 Sklearn 用户指南

交叉验证的另一个好处是它完全消除了随机性。换句话说,你无需担心train_test_split偶然生成过于有利的训练和测试集,这会使模型的目标函数产生偏差。

## 一站式教程:你可以(应该)使用的所有交叉验证技术

作为数据科学家,你需要了解的所有交叉验证程序,解释如下

towardsdatascience.com

你可以从官方用户指南中了解更多关于在代码中实现交叉验证的内容。

6️⃣. 使用准确率作为评估分类器性能的指标

默认情况下,所有 Sklearn 分类器在调用.score函数时使用准确率作为评分方法。由于这种指标的易得性,初学者通常会广泛使用它来评估模型的性能。

不幸的是,原始的准确率 评分仅在一种场景下有用——一个二分类问题,即具有相等、平衡的类别权重。

有时,它是一个极具误导性的指标,甚至最差的模型也可以通过高准确率掩盖其真正的表现。例如,如果模型检测垃圾邮件,它可以在没有发现任何垃圾邮件的情况下达到超过 90%的准确率。

为什么?由于垃圾邮件并不常见,分类器可以检测所有非垃圾邮件,这可以提升其准确率,即使分类器完全未能实现其目的——检测垃圾邮件!

对于多分类问题来说,这个问题更为严重。如果你获得了 80%的准确率,这是否意味着模型在检测类别 1、类别 2、类别 3,还是所有类别上都更准确?

准确率无法回答此类问题,但幸运的是,还有多种其他分类指标可以提供更有信息量的性能总结。你可以在我另一篇文章中阅读这些指标的介绍,文章讨论了适用于二分类和多分类问题的指标:

## 多分类指标综合指南

编辑描述

[towardsdatascience.com

结论

犯错确实很尴尬,但这是旅程的一部分。即使是最有经验的人也会承认他们在自己的时间里曾犯过让他们脸红的错误。

但所有这些指标都有某些人最终指出了它们的错误并展示了正确的处理方式。如果你曾经犯过上述错误,我希望我成为了你心目中的那个人。

感谢阅读!

喜欢这篇文章吗?让我们面对现实,它那奇特的写作风格?想象一下,能访问更多类似的文章,都是由一位才华横溢、迷人、风趣的作者(顺便说一句,就是我 😃)撰写的。

仅需 4.99 美元的会员资格,你将不仅能访问我的故事,还能获得来自 Medium 上最聪明才智的宝贵知识。如果你使用我的推荐链接,你将获得我的超级感激和一个虚拟的击掌,感谢你对我工作的支持。

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

获取对我所有⚡优质⚡内容的独家访问权限,并在 Medium 上无限制阅读。通过购买我的…

ibexorigin.medium.com 支持我的工作

2023 年你必须学习的 6 个新兴数据科学库,以提升你的技能

原文:towardsdatascience.com/6-new-booming-data-science-libraries-you-must-learn-to-boost-your-skill-set-in-2023-106b318d9fa

数据科学不再只是 Pandas、NumPy 和 Scikit-learn

Bex T.Towards Data Science Bex T.

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

--

照片来源 Tobit Nazar Nieto Hernandez

动机

随着 2023 年的到来,现在是时候发现新的数据科学和机器学习趋势了。虽然旧的工具仍然很重要,但仅仅掌握 Pandas、NumPy、Matplotlib 和 Scikit-learn 已经不够了。

去年版本的这篇文章 更多地涉及经典的机器学习,包括像 CatBoost、LightGBM、Optuna 或 UMAP 这样的库。

在 2022 年,我观察到比 2021 年有更多的“我亲爱的联系,我很高兴宣布我是一名 MLOps 工程师”帖子。因此,关于 MLOps 的内容也多了很多,MLOps 工具的受欢迎程度大幅上升。

所以,今年的文章关注 MLOps 生态系统中的六颗新星;这些工具专注于以最有效的方式生成最佳表现的模型,然后将它们投入生产。

1. BentoML

你可能已经厌倦了听到“机器学习模型不在 Jupyter Notebook 中”。如果你还没厌倦,我再说一遍:

机器学习模型不会只存在于 Jupyter 中,积满灰尘。

他们存在于生产环境中,真正执行他们应该做的事情——预测新数据。

我去年发现的最好的模型部署库之一是 BentoML。BentoML 是一个全方位的框架,用于维护、打包和将任何框架的模型部署到任何云提供商作为 API 服务。

它支持以统一格式(版本化和标记)保存/加载模型,使你能够建立一个有组织的模型注册库。

作者截图,已获得 BentoML 的许可

从那里,你可以通过一个命令构建最佳模型的 Docker 镜像并在本地提供服务:

$ bentoml containerize my_classifier:latest
$ docker run -it --rm -p 3000:3000 my_classifier:6otbsmxzq6lwbgxi serve --production

或者使用几个命令将其部署到任何云提供商,而无需离开 CLI。以下是 AWS Sagemaker 的示例:

$ pip install bentoctl terraform
$ bentoctl operator install aws-sagemaker
$ export AWS_ACCESS_KEY_ID=REPLACE_WITH_YOUR_ACCESS_KEY
$ export AWS_SECRET_ACCESS_KEY=REPLACE_WITH_YOUR_SECRET_KEY
$ bentoctl init
$ bentoctl build -b model_name:latest -f deployment_config.yaml
$ terraform init
$ terraform apply -var-file=bentoctl.tfvars -auto-approve

这是一个逐步教程,我展示了如何将 XGBoost 模型部署到 AWS Lambda:

## 完整指南:如何将任何机器学习模型部署为 Python 和 AWS Lambda 的 API

人们说 Python 最好的地方在于其开源库的丰富。我不同意。拥有很多…

towardsdatascience.com

统计数据和链接:

2. MLFlow

在将最佳模型投入生产之前,你必须通过实验来生成它。通常,这可能需要几十次甚至上百次的迭代。

随着迭代次数的增加,跟踪你已经尝试过的配置以及哪些过去的实验看起来有前景变得越来越困难。

为了帮助你完成这个过程,你需要一个可靠的框架来同时跟踪代码、数据、模型、超参数和指标。

手动构建该框架(或像原始人一样使用 Excel)是世界上最糟糕的主意,因为有很多出色的 Python 库可以完成这项工作。

其中之一是 MLFlow,我个人最喜欢的。通过将以下代码行添加到训练 scikit-learn 模型的脚本中,MLFlow 将捕获所有内容——模型本身、其超参数以及使用sklearn.metrics函数计算的任何指标:

mlflow.sklearn.autolog()

一旦你完成了各种尝试,就在终端上运行mlflow ui,它会打开一个实验仪表盘,带有排序和可视化实验的控制功能:

作者截图

MLFlow 有一个mlflow.framework.autolog()功能,支持更多框架,比你能说出的还要多。它简单又实用,让你无法不使用它。

这是我关于该框架的教程,讨论了它的功能以及与数据生态系统中其他工具的集成。

## 完整指南:使用 MLFlow 和 DagsHub 进行实验跟踪

编辑描述

towardsdatascience.com

统计数据和链接:

3. DVC

简而言之,DVC 就是数据的 Git。

作者截图

DVC(数据版本控制)正成为首选的数据和模型版本库。它可以:

  1. 像 Git 追踪轻量级脚本一样追踪千兆字节级的数据集或模型。

  2. 创建主代码库的分支以安全地进行实验,而不必复制大型文件。

当你用dvc add directory追踪一个大文件或目录时,会创建一个轻量级的directory.dvc元数据文件。然后,DVC 将这些轻量文件作为原始重型文件的占位符来管理。

DVC 承担了重担,而 Git 处理小事务,比如你的脚本。它们一起组成了一个完美的组合。

DVC 的另一个卖点是智能工作流管道。一个典型的机器学习工作流涉及到的数据收集、数据清洗、特征工程和模型训练等步骤。

DVC 可以从这些步骤中创建智能管道,这样你只需用两个关键词——dvc repro来运行它们。

聪明的部分是什么?DVC 只执行管道中修改过的步骤,从而节省你数小时的时间和计算资源。

将 MLFlow 添加到你的训练脚本中,使用 DVC 追踪模型工件,这样你就拥有了完美的三重奏(Git、DVC、MLFlow)。

查看我为初学者准备的 DVC 教程开始学习:

## 在 DVC 中进行数据版本控制:了解其他数据科学家忽略的内容

编辑描述

[towardsdatascience.com

统计信息和链接:

[## 通过我的推荐链接加入 Medium - BEXGBoost

获得独家访问所有我的⚡高级⚡内容,并在 Medium 上无限制浏览。请通过给我买杯咖啡来支持我的工作……

ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----106b318d9fa--------------------------------)

4. Weights & Biases

另一个完全开源的实验追踪框架是 Weights & Biases(wandb.ai)。唯一的区别是?它由一家拥有超过 2 亿美元资金的公司提供,该公司的客户包括 OpenAI、NVIDIA、Lyft、BMW、三星等。

他们的主要卖点是:

  • 与 ML 生态系统其他部分的优秀集成,就像 MLFlow 一样

  • 追踪和比较实验的最美观的 UI(个人感觉)

作者截图

  • 协作报告和仪表板

  • 超参数优化(在 MLFlow 中无法实现)

最棒的是,以上所有功能都可以直接通过 Jupyter 使用。这意味着你不需要放弃你喜欢的 IDE 而转到脚本中来跟踪实验。

因此,你的完美组合可能实际上是 Git、DVC 和 Weights & Biases。

统计数据和链接:

5. NannyML

部署模型只是故事的一部分。为了维持成功的机器学习产品,你必须持续监控其性能。

监控的问题在于,当模型失败时你不会收到明显的、红色的错误提示。随着时间的推移,它们的预测可能会变得越来越差,导致一种称为沉默模型失败的现象。

例如,你部署了一个检测 Nike 服装的模型。时尚变化迅速,因此 Nike 不断改进其设计。

由于你的模型训练没有包含新的设计,它开始逐渐错过图像中的 Nike 服装。你不会收到错误提示,但你的模型很快就会变得无用。

NannyML 帮助解决了这个确切的问题。通过使用他们开发的新型基于置信度的性能估计算法和一些其他稳健的统计测试,他们可以在生产环境中检测性能下降或沉默模型失败。

NannyML 还具有智能警报功能,使你始终能够了解生产环境中的发生情况。

这里是一个实用教程,帮助你入门:

## 数据漂移可解释性:使用 NannyML 的可解释性漂移检测

编辑描述

[towardsdatascience.com

统计数据和链接:

6. Poetry

你可能已经听到 Python 程序员对pip及其依赖问题抱怨了无数次,而我也是这些抱怨者之一,直到我看到了 Poetry。

Poetry 是一个改变游戏规则的开源 Python 打包和依赖管理框架。在最简单的用例中,Poetry 可以在你安装库之前检测依赖冲突,从而完全避免依赖地狱。

作者截图

你还可以使用pyproject.toml文件将 Python 项目配置为,Poetry 会通过简单的命令处理虚拟环境、构建和发布到 PyPI。

作者截图

这里有一个全面的 Real Python 教程关于 Poetry:

## 使用 Python Poetry 进行依赖管理 - Real Python

在深入了解 Python Poetry 的细节之前,你需要处理一些前置条件。首先,你需要阅读一篇…

realpython.com

统计数据和链接:

结论

数据科学领域不断发展,新工具和库以惊人的速度不断推出。跟上这些变化的压力比以往任何时候都要大。在这篇文章中,我尽力将你的关注点缩小到 2023 年有望迅速发展的机器学习领域。感谢阅读!

喜欢这篇文章吗?让我们面对现实,它那奇特的写作风格?想象一下,访问更多类似的文章,全由一位聪明、迷人、风趣的作者(顺便说一下,就是我 :)撰写。

仅需 4.99 美元会员费,你将获得不仅仅是我的故事,还有来自 Medium 上最佳和最聪明头脑的宝贵知识。如果你使用我的推荐链接,你将获得我超级 nova 般的感激之情和一个虚拟的击掌,感谢你对我工作的支持。

## 通过我的推荐链接加入 Medium — Bex T.

获取对我所有⚡高级⚡内容的独家访问权限,以及 Medium 上的所有内容,无限制。通过为我购买一份…来支持我的工作。

ibexorigin.medium.com

区分资深开发人员与初级开发人员的 6 个 Python 最佳实践

原文:towardsdatascience.com/6-python-best-practices-that-distinguish-seniors-from-juniors-84199d4cac3c

如何编写被认为来自经验丰富的开发人员的 Python 代码

Tomer GabayTowards Data Science Tomer Gabay

·发表于 Towards Data Science ·12 min 阅读·2023 年 4 月 18 日

--

照片由 Desola Lanre-Ologun 提供,发布于 Unsplash

2023 年 1 月,我发表了 一篇关于区分资深开发人员与初级开发人员的 5 个 Python 技巧。在这篇文章中,我们不关注“技巧”,而是探讨 6 个 Python 最佳实践,这些实践可以区分经验丰富的开发人员和初学者。通过各种示例,我们将深入了解资深开发人员和初级开发人员编写的代码之间的差异。

通过学习这些最佳实践,你可以编写出不仅被视为资深开发人员创作的代码,而且实际上代码质量也会更高。这些特点在向同事或在工作面试中展示你的代码时都会很有优势。

1. 使用正确的可迭代类型

可迭代对象是任何能够逐个返回其成员的 Python 对象,允许在 for 循环中进行迭代。 (source)

初级开发人员往往每次需要可迭代对象时都使用列表。然而,不同类型的可迭代对象在 Python 中有不同的用途。总结最基本的可迭代对象:

  • 列表用于需要排序可变的可迭代对象。

  • 集合是用于仅包含唯一值的可迭代对象,它们是可变无序的。当检查某项是否存在时,它们应该是首选,因为它们非常快速。然而,当用于迭代时,它们比list要慢。

  • 元组是用于有序不可变的可迭代对象。元组比列表更快,更节省内存。

首先让我们看看使用 setlist 的区别。想象一下一个简单的任务:当请求的用户名已被使用时警告用户。例如,你可能经常遇到如下代码:

requested_usernames = ["John123", "Delilah42"]
taken_usernames = []
for username in requested_usernames:
  if username not in taken_usernames:
    taken_usernames.append(username)
  else:
    print(f"Username '{username}' is already taken!")

在上述代码中,taken_usernames 是一个 list。然而,由于 taken_usernames 中的所有值只能出现一次,不需要重复值,因为不允许重复的用户名。因此,这里没有必要使用 list。相反,使用 set 更好,因为我们在上面提到过,当使用 set 时,检查是否存在更快,并且没有必要存储重复的值。

requested_usernams = ["John123", "Delilah42"] 
taken_usernames = set()
for username in requested_usernames:
  if username not in taken_usernames:
    taken_usernames.add(username)
  else:
    print(f"Username '{username}' is already taken!")

尽管这两个代码片段的结果相同,但使用 set 进行存在性检查而不是 list 向他人展示了你理解不同的可迭代类型适用于不同的用例,而不是每次需要可迭代对象时都使用 list

对于在执行期间不会发生变化并且需要顺序的可迭代对象,tuple是最佳选择;例如:

# more junior example
weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]

for day in weekdays:
  ...

--------------------------------------------------------------------------
# more senior example
WEEKDAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")

for day in WEEKDAYS:
  ...

现在你对何时使用哪种可迭代类型有了更好的理解!当你使用 set 来处理仅包含唯一值且不需要顺序的可迭代对象时,你的代码会显得更高级,而 tuple 则用于有序且值不应更改的可迭代对象。

在下一节中,我们将探讨 Python 的命名规范,之后会明确为什么例如 WEEKDAYS 在上面的代码片段中使用了大写字母。

如果你想深入了解何时使用哪种可迭代对象,请参阅例如这篇文章:

15 Examples to Master Python Lists vs Sets vs Tuples

理解这些数据结构之间的区别

towardsdatascience.com

2. 使用 Python 的命名规范

Python 中变量名的‘规则’有两种:

  • 强制规则

  • 命名规范

强制规则防止无效的变量名,例如以数字开头或包含连字符的变量名:

2nd_name = "John"

# output 
SyntaxError: invalid syntax

---------------------------

second-name = "John"

# output 
SyntaxError: invalid syntax

当然,由于这些规则是由 Python 解释器强制执行的,所以(希望)你不会在代码中看到它们的应用。然而,对于变量名,有一些样式指南(即命名约定)是没有强制执行的,因此,你可以对错误的对象使用错误的样式。

以下是 Python 中一些最重要的命名约定:

变量: 仅使用小写字母,并用下划线分隔单词,例如:

  • first_name items names_list

函数方法:变量相同的规则,全部小写,并用下划线分隔单词,例如:

  • get_avg load_data print_each_item

类: 使用 CamelCasing;以大写字母开头,每个新单词也以大写字母开头,中间没有下划线:

  • Person TrainStation MarineAnimal

常量: 仅使用大写字母,并用下划线分隔单词,例如:

  • WEEKDAYS FORBIDDEN_WORDS

模块: 对于 Python 文件名,使用与变量、函数方法相同的约定(小写字母并用下划线分隔单词):

  • calculations.py data_preprocessing.py

使用正确的命名约定不仅显示了你在 Python 中的成熟度,而且不使用正确的命名约定可能导致代码显著混乱,正如下文所示。

遵循 PEP-8 命名约定的 Python 代码:

# circle.py

PI = 3.14 # Value won't change, so it's a constant

class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    @property
    def area(self):
        return (self.radius **2) * PI

    @property
    def perimeter(self):
        return 2 * self.radius * PI

small_circle = Circle(1)
big_circle = Circle(5)

print(f"Area of small circle = {small_circle.area}")
print(f"Perimeter of small circle = {small_circle.perimeter}")

print(f"Area of big circle = {big_circle.area}")
print(f"Perimeter of big circle = {big_circle.perimeter}")

不使用命名约定的 Python 代码:

# CIRCLE.py

Pi = 3.14

class CIRCLE:
    def __init__(Self, RADIUS: float):
        Self.Radius = RADIUS

    @property
    def AREA(Self):
        return (Self.Radius **2) * Pi

    @property
    def PERIMETER(Self):
        return 2 * Self.Radius * Pi

SmallCIRCLE = CIRCLE(1)
BigCIRCLE = CIRCLE(5)

print(f"Area of small circle = {SmallCIRCLE.AREA}")
print(f"Perimeter of small circle = {SmallCIRCLE.PERIMETER}")

print(f"Area of big circle = {BigCIRCLE.AREA}")
print(f"Perimeter of big circle = {BigCIRCLE.PERIMETER}")

如果你提供的代码片段是第二种,那么大多数开发人员肯定会高度怀疑你的 Python 技能,而如果提供的是第一种片段,他们会认为是高质量的 Python 代码。因此,确保遵守 Python 的命名约定是非常重要的。

有关 Python 命名约定的更多详细信息,请参见 PEP-8

3. 使用适当的比较语句

比较运算符(…)比较它们两边的值并返回一个布尔值。它们告诉你一个语句是 True 还是 False,具体取决于条件。 (source)

在 Python 中,有很多种方法来编写(几乎)相同的比较语句,但它们并不一定同样合适。我们来看一个小例子:

def is_even(x):
  return True if x % 2 == 0 else False

x = 10

# different comparison statements which result in the same result:
if is_even(x) == True:
  print(f"{x} is an even number!")

if is_even(x) is True:
  print(f"{x} is an even number!")

if is_even(x):
  print(f"{x} is an even number!")

最后一个例子 if is_even(x): 能够工作,因为在没有任何比较对象的情况下,Python 评估 is_even(x) 是否为 True。然而,需要注意的是,几乎任何 Python 变量都将被评估为 True,除非:

  • 空序列,如列表、元组、字符串等。

  • 数字 0(无论是整数还是浮点数类型)

  • None

  • False(显而易见)

这意味着,例如,每个if <number>:语句将评估为True,除非该数字是0。因此,没有具体示例的 if 语句可能看起来过于宽泛,因为它可能无意中评估为True的可能性很高。然而,我们可以很好地利用空序列始终评估为False这一事实,而至少有一个值的序列始终评估为True。在初级 Python 代码中,你常常会遇到以下比较语句:if <variable> != [],如下片段所示。

def print_each_item(items):
  if items != []:
    for item in items:
      print(item)
  else:
    raise ValueError("'items' is an empty list, nothing to print!")

然而,当有人插入不同类型的可迭代对象时会发生什么呢?例如,一个set。如果你希望对空list引发ValueError,你可能也希望对空set引发ValueError。在上述代码中,空set仍将评估为True,因为空set与空list不相等。避免这种不必要行为的一种方法是使用if items而不是if items != [],因为if items现在将对每个空的可迭代对象引发ValueError,包括listset和第一部分中的tuple

def print_each_item(items):
  if items:
    for item in items:
      print(item)
  else:
    raise ValueError("No items to print inside 'items'")

如果你想显式地将值与TrueFalse进行比较,你应该使用is Trueis False,而不是== True== False。这同样适用于None,因为它们都是单例。见PEP285。虽然性能差异微小,但is True== True稍快。最重要的是,这表明你熟悉 PEP(Python 增强提案),这显示了开发者的技能成熟度。

# more junior example
if is_even(number) == True:

# more senior example
if is_even is True:

-------------------

# more junior example
if value == None:

# more senior example
if value is None:

额外提示: PEP8警告使用if value来确保value不是None。要检查值是否不是None,请明确使用if value is not None

选择最合适的比较语句有时可以避免你或他人调试棘手的错误。但最重要的是,高级开发者会更看重你使用,例如if value is True而非if value == True

当然,与其仅仅编写变量值的比较语句,不如首先检查数据类型,但我们如何为此引发好的异常呢?让我们看看本节中的信息性异常引发方法!

4. 提升信息性异常

初级开发者很少做的一件事是‘手动’引发带有自定义消息的异常。让我们考虑以下情况:我们想创建一个名为print_each_item()的函数,该函数打印每个可迭代类型的项。

最简单的解决方案是:

def print_each_item(items):
  for item in items:
    print(item)

当然,这段代码是有效的。然而,当这个函数是大型代码库的一部分时,运行程序时可能缺少打印结果可能会让人困惑。函数是否被正确调用?解决这种问题的一种方法是使用日志记录,我们将在下一节讨论。首先,让我们看看如何通过抛出Exceptions来防止诸如缺少打印结果等不安全问题。

函数 print_each_item() 仅对可迭代的 Python 对象有效,因此我们首先应该检查 Python 是否可以通过使用内置函数 iter() 对提供的参数进行迭代:

def print_each_item(items):

  # check whether items is iterable
  try:
    iter(items)
  except TypeError as error:
    raise (
      TypeError(f"items should be iterable but is of type: {type(items)}")
      .with_traceback(error.__traceback__)  
    )

通过对 items 尝试 iter() 函数,我们检查是否可以对 items 进行迭代。当然,也可以通过 isinstance(items, Iterable) 来检查 items 的类型,但某些自定义的 Python 类型可能不被算作 Iterable,尽管它们可能是可迭代的,因此 iter(items) 更加可靠。我们在 Exception 中添加 .with_traceback 以提供更多调试时的上下文。

接下来,当我们确认 items 是可迭代的时,我们必须确保 items 不是空的可迭代对象,以防止打印结果缺失。我们可以使用之前学到的 if items: 来实现。如果 if items:False,我们想要抛出一个 ValueError,因为这意味着可迭代对象是空的。下面是经过验证的 print_each_item() 函数:

def print_each_item(items):

  # check whether items is an Iterable
  try:
    iter(items)
  except TypeError as e:
    raise (
      TypeError(f"'items' should be iterable but is of type: {type(items)}")
      .with_traceback(e.__traceback__)  
    )

  # if items is iterable, check whether it contains items
  else:
    if items:
      for item in items:
        print(item)

    # if items doesn't contain any items, raise a ValueError
    else:
      raise ValueError("'items' should not be empty")

当然,最简单的 print_each_item() 适用于大多数用例,但如果你在公司担任开发人员,或编写开源代码,并且该函数经常被重用,你的 Python 同行可能会要求或至少会更高兴看到第二个示例中的函数。能够理解函数可能出现的异常以及如何正确处理它们,并抛出有用的异常,绝对是成为(更)高级开发者所需的技能。

然而,当你的函数被其他人审查时,仍然可能会被拒绝。因为它没有包含文档字符串或任何类型提示,这些都是高质量 Python 代码的必要条件。

5. 类型提示和文档字符串

类型提示在 Python 3.5 中引入。通过类型提示,你可以提示预期的类型。一个非常简单的示例可以是:

def add_exclamation_mark(sentence: str) -> str:
  return f"{sentence}!"

通过指定str作为sentence的类型提示,我们知道sentence应该是一个字符串,而不是例如一个包含单词的列表。通过-> str我们指定该函数返回一个字符串类型的对象。Python 不会强制执行正确的类型(如果插入了不同类型的对象不会引发Exception),但通常像 Visual Studio CodePyCharm 这样的 IDE 会通过利用代码中的类型提示来帮助你编程(见本节进一步下方的截图)。

我们也可以在print_each_item()中应用这一点,通过:

from collections.abc import Iterable

def print_each_item(items: Iterable) -> None:
  ...

文档字符串 有助于解释代码片段,如函数或类。我们可以向print_each_item()添加以下文档字符串,以使其他用户和未来的自己清楚函数的作用:

from collections.abc import Iterable

def print_each_item(items: Iterable) -> None:
  """
  Prints each item of an iterable.

  Parameters:
  -----------
  items : Iterable
      An iterable containing items to be printed.

  Raises:
  -------
  TypeError: If the input is not an iterable.
  ValueError: If the input iterable is empty.

  Returns:
  --------
  None

  Examples:
  ---------
  >>> print_each_item([1,2,3])
  1
  2
  3

  >>> print_each_item(items=[])
  ValueError: 'items' should not be empty
  """
  ...

现在,如果我们编写使用print_each_item的代码,我们会看到以下信息出现:

(截图由作者提供)

通过添加类型提示和文档字符串,我们使我们的函数变得更加用户友好!

关于类型提示的更多信息请点击 这里。有关文档字符串的更多信息请参见 PEP-257

注意: 为这样一个简单的函数编写长文档字符串可能会觉得有些过头,尽管你可以争辩说确实有时是这样。幸运的是,当一个函数不是秘密的时,你可以轻松地请求 ChatGPT 为你编写一个非常准确和详细的文档字符串!

6. 使用日志记录

有几件事可以使使用你的代码对其他人更加愉快,例如类型提示和文档字符串。然而,最重要、最少使用且被低估的特性之一是日志记录。尽管许多(初级)开发者认为日志记录是困难或不必要的,但运行一个适当记录的程序可以对任何使用你的代码的人产生巨大差异。

只需要两行代码即可使日志记录在你的代码中成为可能:

import logging

logger = logging.getLogger(__name__)

现在,你可以轻松地添加日志来帮助,例如调试:

import logging
from collections.abc import Iterable

logger = logging.getLogger(__name__)

def print_each_item(items: Iterable) -> None:
  """
  <docstring>
  """
  logger.debug(
    f"Printing each item of an object that contains {len(items)} items."
  )
  ... 

它还可以通过记录错误信息真正帮助其他开发者进行调试:

import logging
from collections.abc import Iterable

logger = logging.getLogger(__name__)

def print_each_item(items: Iterable) -> None:
  """
  <docstring>
  """

  logger.debug(
      f"Printing each item of an object that contains {len(items)} items."
    )

  # check whether items is iterable
  try:
    iter(items)
  except TypeError as e:
    error_msg = f"'items' should be iterable but is of type: {type(items)}"
    logger.error(error_msg)
    raise TypeError(error_msg).with_traceback(e.__traceback__)

  # if items is iterable, check whether it contains items
  else:
    if items:
      for item in items:
        print(item)

    # if items doesn't contain any items, raise a ValueError
    else:
      error_msg = "'items' should not be empty"
      logger.error(error_msg)
      raise ValueError(error_msg)

因为日志记录在较初级开发者的代码中是一个很少见的特性,添加它到你自己的代码中会让你显得(更)有经验一些!

总结

在这篇文章中,我们回顾了 6 个 Python 最佳实践,这些实践可以让你看起来像是一个资深开发者,而不是一个初级开发者。当然,还有许多其他因素区分资深开发者和初级开发者,不过,通过应用这 6 个最佳实践,你肯定会在你的工作、编程面试或贡献开源项目时,明显区别于那些不应用这些最佳实践的初级开发者!

要了解高级和初级代码之间的差异,请查看:

## 5 个 Python 技巧,区分高级开发者和初级开发者

通过 Advent of Code 题目的解法差异进行说明

towardsdatascience.com

如果你想了解如何自己晋升为更高级别的开发者,请查看:

## 如何通过向这些专业人士学习来提升你的 Python 技能

避免停留在 Python 编程的初级水平

towardsdatascience.com

资源

可迭代对象 www.pythonlikeyoumeanit.com/Module2_EssentialsOfPython/Iterables.html#

stackoverflow.com/questions/2831212/python-sets-vs-lists

towardsdatascience.com/15-examples-to-master-python-lists-vs-sets-vs-tuples-d4ffb291cf07

命名约定

peps.python.org/pep-0008

www.techtarget.com/whatis/definition/CamelCase#:~:text=CamelCase%20is%20a%20way%20to,humps%20on%20a%20camel's%20back.

适当的比较语句 peps.python.org/pep-0008

peps.python.org/pep-0285/

flexiple.com/python/comparison-operators-in-python/

类型提示和文档字符串 docs.python.org/3/library/typing.html

peps.python.org/pep-0257/

日志记录 docs.python.org/3/library/logging.html

表情包 www.reddit.com/r/ProgrammerHumor/comments/l9lbm2/code_review_be_like/

使数据可靠性成为习惯的 6 个步骤

原文:towardsdatascience.com/6-steps-to-making-data-reliability-a-habit-521389b04aca?source=collection_archive---------10-----------------------#2023-02-10

一些组织对待数据质量的方式就像快速减肥。然而,试试这 6 个步骤,实现可持续的数据质量管理。

Barr MosesTowards Data Science Barr Moses

·

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

--

图片来源:Bruno NascimentoUnsplash

我喜欢将数据可靠性的操作化视为一种身体健康的管理。

你可以通过努力工作来保持身体健康,但保持健康需要良好的习惯。 最重要的是,这是一种心态和生活方式。

你也需要付出努力。至少一点点。没有任何技术,包括数据可观察性,能像瘦身带那样在你放松时提升数据质量。

也有很多关于数据质量的错误建议。与其关注 7 分钟腹肌,不如关注数据质量的 6 个维度。是的:完整性、一致性、准确性、有效性、完整性和唯一性都很重要,但这些都是情境性的诊断快照。而且这些在董事会上从未被提及。

随着我们稳步进入数据云时代,数据领导者需要与最终(数据)产品一样关注机器的鲁棒性和可靠性——数据管道、系统和工程师。

最重要的是,数据质量管理需要有一个可重复的过程,而不仅仅是静态的数据清理、测试和分析。这些传统方法无法在现今拥有数十个数据源、数百个数据模型、数千个表和受到运营用例影响的数百万美元的组织中扩展。

在过去三年中,我观察了数百个已经实现数据质量的团队。以下是最成功团队所经历的 6 个数据质量成熟阶段。

阶段 1:基线当前数据可靠性状态

启动数据质量管理策略的最佳地方之一是对你当前(以及理想的近期)数据使用案例进行盘点。按以下类别进行分类:

  • 分析性:数据主要用于决策或通过 BI 仪表盘评估不同业务策略的效果。这一直是组织内数据最传统的使用方式,并且今天仍然是最突出的用例之一。虽然你可以选择更详细的方式,有时快速的大致描述对基线而言更好。要确定每个分析用例是“可有可无”还是“必不可少”,大致评估数据消费者的数量和它帮助优化的操作的商业价值。

  • 操作性:数据直接用于支持接近实时的业务操作。这通常是流数据或微批处理数据。一些用例可能包括在支持/服务过程中接待客户,或一个电子商务机器学习算法推荐“你可能喜欢的其他产品”。

  • 面向客户:在产品中显示并为产品提供价值的数据,或本身就是产品的数据。例如,这可以是数字广告平台中的报告套件。

为什么这很重要?正如前面提到的,数据质量是情境性的。在某些场景中,例如财务报告,准确性至关重要。其他用例,例如一些机器学习应用,新鲜度将是关键,“方向上准确”将足够。

下一步是评估系统和团队的整体表现。在这一阶段,你刚刚开始你的旅程,因此不太可能对整体数据健康状况或操作有详细的了解。然而,你可以使用一些定量和定性的代理指标。

  • 定量: 你无法测量那些你未捕捉到的数据事件的数量,但可以通过计算环境中的表格数量并除以 15 大致估计每年的数据事件数量。你可以测量数据消费者的投诉数量、整体数据采纳情况以及数据信任水平(NPS 调查)。你还可以让团队估算他们在数据质量管理相关任务上花费的时间,如维护数据测试和解决事件。

  • 定性: 是否有进一步的数据高级用例的需求或机会?领导者是否感觉他们已经挖掘出了组织数据的全部价值?文化是否以数据驱动?是否有最近的数据质量灾难导致了非常高层的升级?

对数据用例进行分类并基准当前表现,也将帮助你评估基础设施、团队、流程和表现之间的当前状态和期望未来状态的差距。这是对影响数据质量的更广泛战术问题的回答:

技术

  • 我是否需要将任何本地数据库迁移到云端?

  • 我们需要在哪里使用数据流处理与批处理?ETL 还是 ELT?

  • 我如何优先考虑并构建现代数据平台的层级,包括数据摄取、存储、转换/编排、可视化、数据质量、数据治理/访问管理?

  • 我们需要多大的数据管道监控覆盖率?

人员

  • 应该设立一个中央数据团队、去中心化的数据网格,还是一个带有卓越数据中心的混合模式?

  • 我是否需要专门的角色和/或团队来管理数据治理,例如数据管理员,或数据质量,例如数据可靠性工程师?

流程

  • 我们在识别数据事件根本原因方面是否高效?

  • 我们是否理解每个资产的相对重要性及其相互关系?

  • 我们应当制定哪些数据服务级别协议(SLA)?

  • 我们如何引入数据集?

  • 适当的文档水平是什么?

  • 我们如何促使发现并优先考虑数据的自助访问?

“鉴于我们在金融行业,我们看到分析和操作报告的用例非常不同,这要求我们达到很高的准确性,”Checkout.com 高级数据工程师 Martynas Matimaitis 告诉我们。“这迫使我们在旅程的早期阶段就开始[扩展数据质量管理],这成为了我们日常业务的关键部分。”

阶段 2:组织对齐

一旦你有了基准和一个有根据的意见,你就可以开始为你的计划建立支持。你需要了解不同利益相关者感受到的痛点。

这将帮助你合理调整你的计划,并将目标与业务价值对齐。我建议考虑将数据停机时间作为关键数据质量指标,但最终最好的指标是那个测量你老板和客户关心的东西的指标

如果没有痛点,你需要花时间理解原因。可能是你的数据操作规模或数据的重要性还不够成熟,不值得投资于提高数据质量。然而,假设你有超过 50 个表格和一些数据团队成员,这种情况不太可能发生。

更可能的是,你的组织有相当多的未实现的风险。数据质量低,昂贵的数据事件就在眼前……但尚未发生你的数据消费者通常会信任数据,直到你给他们一个理由不信任。到那时,恢复信任比失去它要困难得多。

数据信任是数据可靠性的滞后指标。图片由 Shane Murray 提供。

评估数据质量差的总体风险可能很困难。不良数据的后果可能从稍微优化不足的决策到向华尔街报告错误数据不等。一种方法是通过估算你的数据停机时间并将其归因于低效成本来汇总这一风险。或者你可以参考行业的基准——我们的研究显示不良数据平均能影响公司 26%的收入。

风险评估和商业利益相关者处理不良数据的成本虽然信息丰富但可能有些模糊。它还应该与数据团队处理不良数据的成本相结合。这可以通过汇总在数据质量相关任务上花费的时间,皱眉头,然后将这些时间乘以平均数据工程薪资来实现。

专业提示:数据测试通常是数据团队最大效率低下的环节之一。定义、维护和扩展每个数据集中的每一个期望和假设都是耗时的。更糟糕的是,由于数据可能以几乎无限的方式破坏(未知的未知),这种覆盖程度通常是极其不足的。

恭喜!你现在有了一个关于数据质量管理计划的业务案例,以及你需要在人员、技术和流程方面做出的改变。

此时,以下阶段将假设你已经获得了授权,并决定构建 或获取数据质量或数据可观测性解决方案以协助你的工作。现在,是时候实施和扩展了。

阶段 3:广泛的数据质量覆盖和全面的可见性

第三阶段的数据质量管理是确保你在数据环境中有基本的机器学习监控(新鲜度、体量、架构)。许多组织(排除最大企业)将希望更广泛地推出,而不是采取试点再扩展的方法。

这将加速你的价值实现时间,并帮助你建立与不同团队的关键接触点,如果你还没有这样做的话。

更广泛推广的另一个原因是,即使在最分散的组织中,数据也是相互依赖的。如果你在客厅安装了灭火系统,而厨房正在着火,这并没有多大用处。

此外,大规模的数据监控和/或数据可观测性将为你提供关于数据环境和整体健康的完整视图。拥有 30,000 英尺的视角在进入下一阶段的数据质量管理时非常有帮助。

“通过…广泛覆盖和自动化血统…我们的团队可以以更快的速度识别、理解下游影响、优先处理并解决数据问题,” Ashley VanName 说道,她是 JetBlue 的数据工程总经理。

阶段 4:事件分类和解决

在这一数据质量管理阶段,我们希望开始优化事件分类和解决响应。这涉及到建立明确的责任划分。

数据质量应该有团队负责人,并且在数据产品甚至数据管道层面也应该有整体数据资产负责人。如果你还没有将环境划分为多个领域,这可以帮助为不同团队维护的整体数据健康水平创建额外的责任和透明度。

拥有明确的责任划分也能让你更好地调整警报设置,确保警报被发送到负责团队的正确沟通渠道,并在适当的升级层级进行处理。

“我们开始建立这些关系,现在我知道谁是推动数据集的团队,” Lior Solomon 说道,他是 Drata 的数据副总裁。“我可以设置这些 Slack 频道,将警报发送到这些频道,并确保相关方和发布者都在这些频道上,我们可以进行全面的讨论,以确定问题是否需要调查。”

阶段 5:定制数据质量监控

这一数据质量管理阶段专注于叠加更复杂的自定义监控器。这些监控器可以是手动定义的,例如,如果数据需要在每个工作日早上 8:00 是最新的,供一位细致的高管使用,或者基于机器学习的。

后者通常涉及指明哪些表或数据段需要重点检查,并设置机器学习警报,当数据开始出现异常时触发。

我们建议在组织最关键的数据资产上叠加自定义监控器(或测试)。 这些通常可以识别为有许多下游消费者或重要依赖项的数据。

自定义监控器和 SLA 还可以围绕不同的数据可靠性等级构建,以帮助设定期望。你可以将最可靠的数据集认证为“黄金”,或将用于有限用例的临时数据拉取标记为“铜”,以表明其支持程度较弱。

最复杂的组织通过代码(监控即代码)来管理大部分自定义数据质量监控,作为 CI/CD 过程的一部分。

Checkout.com 的数据团队通过将监控即代码功能添加到每个部署管道中,减少了对手动监控器和测试的依赖。这使他们能够在 dbt 存储库中部署监控器,从而帮助协调和扩展数据平台。

“监控逻辑现在是同一个存储库的一部分,并且与数据管道堆叠在一起,成为每一个部署的不可或缺的一部分,”马丁纳斯说道。

此外,这种集中监控逻辑使所有监控器和问题的清晰和便捷展示成为可能,从而加快了解决问题的时间。

阶段 6:事件预防

到这一点,你已经为业务带来了显著的价值,并显著改善了组织的数据质量管理。之前的数据质量管理阶段帮助我们显著缩短了检测时间和解决时间,但数据停机公式中还有第三个变量:数据事件的数量。

本数据质量管理阶段的主要目标之一是开始将数据质量向左移动,并使预防性维护操作化。换句话说,就是在管道发生故障之前预防数据事件。

这可以通过关注数据健康见解,例如未使用的表或恶化的查询来实现。分析和报告不同领域的数据质量水平或服务水平协议(SLA)遵守情况,也可以帮助数据领导者确定资源分配的方向。

“…数据血缘图突出显示了我们数据生态系统中的上下游依赖关系,包括 Salesforce,帮助我们更好地理解数据健康状况,” Moon Active 的商业分析组负责人 Yoav Kamin 说道。“我们不再是被动地在仪表盘出现故障后修复,[我们]拥有了需要的可见性,能够主动进行管理。”

最后的想法

我们在本文中涵盖了很多内容——有些人可能会称之为数据可靠性马拉松。我们的一些关键数据质量管理要点包括:

  • 确保你监控数据管道及其流动的数据。

  • 你可以通过了解团队修复管道所花费的时间以及这对业务的影响来为数据监控构建业务案例。

  • 你可以选择构建或购买数据监控解决方案——选择权在你——但如果你决定购买解决方案,请确保评估其端到端的可视性、监控范围和事件解决能力。

  • 通过从广泛覆盖开始来使数据监控系统化,并随着时间的推移逐步完善警报、责任、预防性维护和程序化操作。

也许最重要的一点是,数据管道会出现故障,数据也会“变坏”——除非你保持其健康。

无论你下一步的数据质量管理措施是什么,最好尽早采取行动。你会感激我们的。

Will Robins 共同撰写了本文。

对讨论如何使数据可靠性操作化感兴趣?请联系 Barr ,或 与我们的团队交谈

6 种聚类方法 — 概述

原文:towardsdatascience.com/6-types-of-clustering-methods-an-overview-7522dba026ca

聚类方法和算法的类型以及何时使用它们

Kay Jan WongTowards Data Science Kay Jan Wong

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

--

Kier in Sight 拍摄的照片,发布于 Unsplash

聚类是无监督学习的一个分支,其中未标记的数据被划分为不同的组,相似的数据实例被分配到同一聚类,而不相似的数据实例则被分配到不同的聚类。聚类在市场细分、异常检测和网络分析等领域有广泛的应用。

不同类型的聚类方法各有其优缺点。本文介绍了不同类型的聚类方法及算法示例,以及何时使用每种算法。

目录

  • 基于质心/划分 (K-means)

  • 基于连通性的 (层次聚类)

  • 基于密度的 (DBSCAN)

  • 图基的 (亲和传播)

  • 基于分布的 (高斯混合模型)

  • 基于压缩的 (谱聚类, BIRCH)

基于质心/划分

基于质心的方法通过数据点到质心(聚类中心)的接近程度来将数据点分组

图 1:基于质心的方法(K-means)的聚类输出示例 — 图片来自 sklearn

数据点到质心的接近程度通过以下方式测量

  • 欧几里得距离:最短(直线)距离,适用于数值特征,这是默认的距离度量

  • 曼哈顿距离:绝对差异的总和,适用于分类特征

  • 汉明距离:不同位数的百分比,适用于二进制特征

  • 马哈拉诺比斯距离:欧几里得距离的标准化形式

  • 闵可夫斯基距离:欧几里得距离和曼哈顿距离的广义形式

  • 切比雪夫距离:最大绝对差异

无论使用哪种距离度量,执行聚类时数值特征都应始终标准化!

K-means

K-means 涉及初始化并执行迭代期望最大化(EM)步骤,直到收敛或达到最大迭代次数。

在初始化期间,k 个质心是根据 k-means++ 优化算法分配的。在期望(E)步骤中,数据点根据其最近的(欧几里得距离)质心分配到聚类中。在最大化(M)步骤中,质心被更新以最小化惯性或聚类内平方和(WCSS)。EM 步骤重复进行,直到收敛到局部最小值,聚类分配和质心不再变化。

何时使用 K-means

  • 你希望可解释性:K-means 易于理解和解释。

  • 聚类是均匀大小和球形的:K-means 在聚类是良好分离的球形时表现良好,但如果聚类是长而不规则的形状,它的表现则不佳。

何时不要使用 K-means

  • 不确定聚类的数量:K-means 需要预定义聚类的数量。通常使用肘部法,绘制 WCSS 与聚类数量的关系图,以确定最优的聚类数量。

  • 你的数据中有离群点:所有数据点都被分配到一个聚类中,因此离群点的存在可能会扭曲结果,必须去除或转换。

  • 你希望计算效率:K-means 的计算成本随数据规模增加而增加,因为它的运行时间是 O(tkn),其中 t 是迭代次数,k 是聚类数量,n 是数据点数量。使用降维算法如 PCA 可以加速计算。

其他基于质心的算法:K-Medoids、均值漂移、模糊聚类(软 K-means;数据点可以属于多个聚类)

编辑:还有方差比准则(VRC)和贝叶斯信息准则(BIC)作为比肘部法更鲁棒的替代方法(感谢 Victor 提供的信息 — 论文链接到 paper)。

基于连通性

基于连通性的方法根据聚类之间的接近度将数据点分组在一起

图 2:基于连通性的方法(层次聚类)的聚类输出示例 — 图片来自 sklearn

连结标准计算聚类之间的接近度,

  • 单一连结:聚类间最接近点的距离;聚类间的最小距离。适用于检测任意形状的聚类,但无法检测重叠的聚类。计算效率高,但对噪声数据不够鲁棒。

  • 完全/最大连结:聚类间最远点之间的距离;聚类间的最大距离。适用于检测重叠聚类,但无法检测任意形状的聚类

  • 平均连结:两个聚类间所有距离的平均值

  • 质心连结:两个聚类中心之间的距离

  • Ward 连结:每个数据点到其分配聚类的质心的平方距离总和。这导致合并聚类时,所有聚类内总方差增加最小。Ward 连结是默认的连结标准

层次聚类

凝聚层次聚类通过进行迭代的自下而上方法工作,其中每个数据点被视为一个单独的聚类,并且两个最接近(按连接标准)的聚类被迭代合并,直到只剩下一个大聚类。

分裂层次聚类的做法正好相反,执行迭代的自上而下方法,从一个大聚类开始,不断分解为两个较小的聚类,直到每个数据点成为一个单独的聚类。

何时使用层次聚类

  • 你对聚类数量不确定:层次聚类需要预定义聚类数量,但可以在树状图上查看聚类,并且可以在不重新计算的情况下调整聚类数量。

  • 你想要计算效率:在任何中间阶段的聚类标签可以被恢复,因此如果聚类数量改变,则不必重新计算相似度矩阵。

  • 你有高维数据:输出可以通过树状图进行可视化,这可以用于更高维的数据。

基于密度

基于密度的方法根据密度而不是距离将数据点分组

图 3:基于密度的方法(DBSCAN)的聚类输出示例 — 图片来自 sklearn

基于密度的空间聚类应用于噪声(DBSCAN)

DBSCAN 算法将密集区域中的数据点聚集在一起,用低密度区域分隔开。位于eps单位内且有超过min_samples数量的数据点的样本称为核心样本

何时使用 DBSCAN

  • 你对聚类数量不确定:DBSCAN 不需要预定义聚类数量。

  • 你想要任意形状的聚类:聚类由密集区域决定,因此聚类形状可以是奇怪或复杂的。

  • 你想要检测离群点:那些未被分配到任何聚类的数据点,因为它们不落在任何密集区域,将被视为离群点。

何时不使用 DBSCAN

  • 当你想要稳定的性能:DBSCAN 的输出受到eps参数的高度影响和敏感,必须为数据集选择适当的eps值。

其他基于密度的算法:排序点以识别聚类结构(OPTICS)、层次 DBSCAN(HDBSCAN)

基于图

基于图的方法根据图距离将数据点分组。

图 4:基于图的方法(亲和传播)的聚类输出示例 — 图片来自 sklearn

亲和传播

亲和传播通过数据点之间的成对消息传递直到收敛。选择代表周围数据点的最佳点作为样本点,并将每个点分配到其最近样本点的簇中。

何时使用亲和传播

  • 不确定簇的数量:亲和传播不要求预定义簇的数量。

何时不使用亲和传播

  • 你想要计算效率:亲和传播的时间复杂度为O(tn²),其中t是迭代次数,n是数据点数量,因为消息在每个数据点之间成对发送。

  • 你想要空间效率:如果使用密集相似度矩阵,亲和传播会占用O(n²)的内存,因为在每个数据点之间发送的消息被视为‘投票’,以选举样本点。

基于分布

基于分布的方法根据数据点属于同一概率分布的可能性将数据点分组。

图 5:基于分布的方法(GMM)的聚类输出示例 — 图片来自 sklearn

基于分布的方法使用统计推断来对数据进行聚类,使得数据点距离中心点越近,被分配到该簇的概率越高。

与考虑距离度量的聚类方法相比,基于分布的方法在确定簇的形状方面更具灵活性,前提是簇遵循预定义的分布,如合成数据或模拟数据。如果簇是噪声的,结果会过拟合。

高斯混合模型(GMM)

GMM 假设每个数据点都是由多个具有未知参数的高斯分布生成的,并通过迭代的期望最大化(EM)步骤来拟合数据点。

在期望(E)步骤中,数据点被分配到假设随机选择的高斯参数的簇中。在最大化(M)步骤中,高斯分布的参数被更新,以最佳地拟合簇中的数据点。

何时使用高斯混合模型

  • 你想要计算效率:GMM 是最快的基于分布的算法。

  • 你的簇遵循高斯分布:分布可以调整以适应球形、对角线、绑定或完全协方差矩阵。

何时不使用高斯混合模型

  • 你有每个簇的数据不足:当高斯分布中的数据点不足时,计算协方差矩阵会很困难。

基于压缩的

基于压缩的方法将数据点转换为嵌入,然后在低维数据上执行聚类。

谱聚类

谱聚类将数据点之间的亲和矩阵转换为低维嵌入,然后再进行聚类(如 K 均值)。

何时使用谱聚类

  • 你希望计算效率:如果亲和矩阵稀疏且使用pyamg求解器,谱聚类速度很快。

何时不使用谱聚类

  • 不确定簇的数量:谱聚类需要预定义簇的数量,并且簇的数量应该尽可能小。

BIRCH

BIRCH 对数据点进行有损压缩,生成一组聚类特征节点(CF Nodes),形成聚类特征树(CFT)。新数据点被‘插入’到树中,直到到达叶子节点,并在节点内存储有关子簇的信息。

最终簇质心的信息从叶子节点读取,其他聚类算法(如层次聚类)可以随之执行。

何时不使用 BIRCH

  • 你有高维数据:BIRCH 对高维数据的扩展性较差。

有趣的是,通过完全不同的方法和算法可以实现获取簇的最终目标。本文并不旨在覆盖所有可能的聚类算法或深入探讨每种算法中的数学公式,但希望能提供一些关于聚类方法类型及其应用场景的高层次细节。

相关故事

聚类的另一个方面是评估聚类算法,以确定哪个算法在统计度量上表现最佳

## 7 聚类算法评估指标

使用 Python 示例深入解释无监督学习评估指标

towardsdatascience.com

相关链接

6 个值得更多关注的数据科学库

原文:towardsdatascience.com/6-underdog-data-science-libraries-that-deserve-much-more-attention-ef64187b562a

该是走出阴影的时候了

Bex T.Towards Data Science Bex T.

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 4 月 13 日

--

图片由我通过 Midjourney 制作。

当像 Pandas、Scikit-learn、NumPy、Matplotlib、TensorFlow 等大牌库占据你所有注意力时,很容易忽视一些接地气且令人惊叹的库。

他们可能不是 GitHub 的明星开发者,也没有在昂贵的 Coursera 专业课程中教授,但成千上万的开源开发者却将他们的心血和汗水倾注于编写这些库。它们在默默无闻中填补了流行库留下的空白。

本文的目的是让大家关注这些库,并一起惊叹开源社区的强大。

让我们开始吧!

0. Manim

图片来源于 Manim GitHub 页面。MIT 许可证。

我们都对 3Blue1Brown 视频的美丽感到惊叹。但我们大多数人不知道,所有的动画都是由 Grant Sanderson 本人编写的 Mathematical Animation Engine(Manim)库创建的。(我们对 Grant Sanderson 的贡献常常视为理所当然。)

每个 3b1b 视频都由 Manim 中数千行代码提供支持。例如,传奇系列“Calculus 的本质”花费了 Grant Sanderson 超过 22,000 行代码。

在 Manim 中,每个动画由一个场景类表示,如下所示(如果你不理解也不用担心):

import numpy as np
from manim import *

class FunctionExample(Scene):
    def construct(self):
        axes = Axes(...)
        axes_labels=axes.get_axis_labels()

        # Get the graph of a simple functions
        graph = axes.get_graph(lambda x: np.sin(1/x), color=RED)
        # Set up its label
        graph_label = axes.get_graph_label(
            graph, x_val=1, direction=2 * UP + RIGHT,
           label=r'f(x) = \sin(\frac{1}{x})', color=DARK_BLUE
        )

        # Graph the axes components together
        axes_group = VGroup(axes, axes_labels)

        # Animate
        self.play(Create(axes_group), run_time=2)
        self.wait(0.25)
        self.play(Create(graph), run_time=3)
        self.play(Write(graph_label), run_time=2)

这会生成 sin(1/x) 函数的以下动画:

GIF 由作者使用 Manim 制作。

不幸的是,Manim 的维护和文档并不好,理解也可以理解,因为 Grant Sanderson 大部分精力都投入到制作精彩的视频中。

不过,Manim Community 社区对该库进行了分叉,提供了更好的支持、文档和学习资源。

如果你已经非常兴奋(你这个数学爱好者!),这里是我对 Manim API 的温和而全面的介绍:

## 如何在 Python 中创建像 3Blue1Brown 一样的光滑数学动画

编辑描述

towardsdatascience.com

统计数据和链接:

由于其陡峭的学习曲线和复杂的安装过程,Manim 每月下载量很少。它值得更多的关注。

1. PyTorch Lightning

PyTorch Lightning GitHub 页面。 的截图 Apache-2.0 许可证。

当我在 TensorFlow 之后开始学习 PyTorch 时,我变得非常不高兴。很明显 PyTorch 很强大,但我忍不住说“TensorFlow 做得更好”,或者“在 TF 中会更简洁”,甚至更糟的是,“我几乎希望我从未学习过 PyTorch”。

这因为 PyTorch 是一个低级库。是的,这意味着 PyTorch 让你对模型训练过程有完全的控制,但这也需要大量的样板代码。如果我没记错的话,它就像 TensorFlow 但年轻了五年。

结果发现,有很多人都有这样的感觉。更具体地说,几乎 830 名 Lightning AI 的贡献者开发了 PyTorch Lightning。

GIF 来源于 PyTorch Lightning GitHub 页面。 Apache-2.0 许可证。

PyTorch Lightning 是一个围绕 PyTorch 构建的高级封装库,它抽象了大部分样板代码,并且缓解了所有的痛点:

  • 硬件无关的模型

  • 代码高度可读,因为工程代码由 Lightning 模块处理

  • 灵活性保持不变(所有 Lightning 模块仍然是 PyTorch 模块)

  • 多 GPU、多节点、TPU 支持

  • 16 位精度

  • 实验跟踪

  • 早期停止和模型检查点(终于来了!)

以及其他接近 40 个高级功能,所有这些功能旨在让 AI 研究人员感到愉悦,而不是让他们恼火。

统计数据和链接:

从官方教程中学习:

[## PyTorch Lightning 教程

Lightning 速度的视频,从零基础到 Lightning 英雄。

www.pytorchlightning.ai](https://www.pytorchlightning.ai/tutorials?source=post_page-----ef64187b562a--------------------------------)

2. Optuna

是的,使用 GridSearch 进行超参数调优很简单、舒适,只需一个导入语句。但你必须承认,它比醉酒的蜗牛还慢,效率极低。

图片由我通过 Midjourney 制作。

暂时把超参数调优想象成购物。使用 GridSearch 意味着要走遍超市的每一个过道并检查每一件商品。这是一种系统性和有序的方法,但你浪费了很多时间。

另一方面,如果你有一个具有贝叶斯根源的智能个人购物助手,你将准确知道你需要什么以及去哪儿。这是一种更高效、更有针对性的方法。

如果你喜欢那个助手,它的名字是 Optuna。它是一个贝叶斯超参数优化框架,用于高效地搜索给定的超参数空间,并找到能提供最佳模型性能的黄金超参数组合。

这里是它的一些最佳功能:

  • 与框架无关:调整你能想到的任何机器学习模型

  • Pythonic API 来定义搜索空间:Optuna 让你从给定范围内线性、随机或对数地采样超参数值,而不是手动列出可能的值

  • 可视化:支持超参数重要性(平行坐标)图、历史图和切片图

  • 控制迭代次数或持续时间:设置精确的迭代次数或调优过程持续的最长时间

  • 暂停并恢复搜索

  • 剪枝:在试验开始之前停止那些不太可能的试验

所有这些功能都旨在节省时间和资源。如果你想看到它们的实际应用,可以查看我的 Optuna 教程(这是我在 150 篇文章中表现最好的之一):

## 为什么 Kaggle 上的每个人都对 Optuna 用于超参数调优情有独钟?

编辑描述

towardsdatascience.com

统计信息和链接:

3. PyCaret

PyCaret GitHub 页面 的截图。MIT 许可证。

我对 Moez Ali 充满敬意,他从零开始独自创建了这个库。目前,PyCaret 是现有的最佳低代码机器学习库。

如果 PyCaret 在电视上做广告,它的广告词会是这样的:

“你是否厌倦了在机器学习工作流中花费几个小时编写几乎相同的代码?那么,PyCaret 就是答案!

我们的一体化机器学习库帮助你用尽可能少的代码行构建和部署机器学习模型。可以把它看作是包含你所有喜欢的机器学习库代码的鸡尾酒,如 Scikit-learn、XGBoost、CatBoost、LightGBM、Optuna 等。

然后,广告将展示这段代码,并伴有戏剧性的声音效果来显示每一行:

# Classification OOP API Example

# loading sample dataset
from pycaret.datasets import get_data
data = get_data('juice')

# init setup
from pycaret.classification import ClassificationExperiment
s = ClassificationExperiment()
s.setup(data, target = 'Purchase', session_id = 123)

# model training and selection
best = s.compare_models()

# evaluate trained model
s.evaluate_model(best)

# predict on hold-out/test set
pred_holdout = s.predict_model(best)

# predict on new data
new_data = data.copy().drop('Purchase', axis = 1)
predictions = s.predict_model(best, data = new_data)

# save model
s.save_model(best, 'best_pipeline')

讲述者会在代码显示时进行旁白:

“只需几行代码,你就可以从不同框架中的几十个模型中训练和选择最佳模型,评估它们并保存以供部署。使用起来非常简单,任何人都可以做到!

赶紧从 GitHub 或通过 PIP 获取我们的软件,之后再感谢我们!

统计数据和链接:

4. BentoML

Web 开发人员像宠物一样喜欢 FastAPI。它是最受欢迎的 GitHub 项目之一,确实使 API 开发变得极其简单和直观。

因为这种受欢迎程度,它也进入了机器学习领域。工程师们常常使用 FastAPI 将他们的模型部署为 API,以为整个过程不会更好或更简单。

但大多数人都抱有幻想。仅仅因为 FastAPI 比它的前身(Flask)好得多,并不意味着它是最适合这个工作的工具。

那么,最适合这个工作的工具是什么?我很高兴你问了——就是 BentoML!

BentoML 尽管相对年轻,但它是一个端到端的框架,用于将任何机器学习库的模型打包并部署到任何云平台。

来自 BentoML 首页 的图像,已获许可。

FastAPI 是为 Web 开发人员设计的,因此在部署 ML 模型方面有许多明显的不足。BentoML 解决了这些问题:

  • 标准 API 用于保存/加载模型

  • 模型存储 用于版本控制和跟踪模型

  • 通过一行终端代码实现模型的 Docker 化

  • GPU 上服务模型

  • 通过一段简短的脚本和几个终端命令将模型作为 API 部署到任何云服务提供商

我已经写了一些关于 BentoML 的教程。这里是其中之一:

## 2022 年最简单的机器学习模型部署方法:Streamlit + BentoML + DagsHub

编辑描述

towardsdatascience.com

统计数据和链接:

5. PyOD

图片由我通过 Midjourney 制作。

这个库被视为黑马,因为它解决的问题,异常检测,也同样是不被看好的。

几乎所有你参加的机器学习课程只教 z 分数来进行异常检测,然后转向更高级的概念和工具,如 R(讽刺)。

但异常检测远不止普通的 z 分数。它包括修改的 z 分数、隔离森林(酷名字)、KNN 异常检测、局部异常因子,以及 30 多种最先进的异常检测算法,这些都打包在 Python 异常检测工具包(PyOD)中。

当没有被正确检测和处理时,异常值会扭曲特征的均值和标准差,并在训练数据中产生噪声——这是你绝对不希望发生的情况。

这就是 PyOD 的生活目标——提供工具来帮助发现异常。除了其广泛的算法,它还与 Scikit-learn 完全兼容,使其易于在现有的机器学习管道中使用。

如果你仍然没有被异常检测的重要性和 PyOD 在其中的作用所说服,我强烈建议你阅读这篇文章(由我撰写):

[## 如何在 Python 中进行异常检测以用于机器学习:第一部分]

编辑描述

towardsdatascience.com](/how-to-perform-outlier-detection-in-python-in-easy-steps-for-machine-learning-1-8f9a3e6c88b5?source=post_page-----ef64187b562a--------------------------------)

统计和链接:

6. Sktime

图片来自 Sktime GitHub 页面。BSD-3 条款许可。

时间机器不再是科幻小说中的东西。它以 Sktime 的形式成为现实。

与其在时间段之间跳跃,Sktime 执行的是稍微不那么酷的时间序列分析任务。

它借用了其“大哥” Scikit-learn 的最佳工具,来执行以下时间序列任务:

  • 分类

  • 回归

  • 聚类(这个挺有趣的!)

  • 注释

  • 预测

它提供了超过 30 种最先进的算法,具有熟悉的 Scikit-learn 语法,还支持管道化、集成和模型调优,适用于单变量和多变量时间序列数据。

它也得到了很好的维护——Sktime 的贡献者们像蜜蜂一样忙碌。

这里有一个关于它的教程(不是我的,遗憾的是):

[## 使用 sktime 构建复杂的时间序列回归管道]

编辑描述

towardsdatascience.com](/build-complex-time-series-regression-pipelines-with-sktime-910bc25c96b6?source=post_page-----ef64187b562a--------------------------------)

统计数据和链接:

包装

虽然我们的日常工作流程主要由像 Scikit-learn、TensorFlow 或 PyTorch 这样的流行工具主导,但重要的是不要忽视那些鲜为人知的库。

它们可能没有相同的认可度或支持,但在合适的使用者手中,它们能提供优雅的解决方案,解决那些流行对手未能解决的问题。

这篇文章仅关注了 个,但你可以肯定还有数百个其他库。你只需做些探索!

喜欢这篇文章以及,它那奇特的写作风格?想象一下,拥有更多类似的文章,全部由一位才华横溢、迷人、机智的作者(顺便说一下,就是我 😃)撰写。

只需 4.99$ 会员资格,你不仅可以访问我的故事,还能获得 Medium 上最优秀、最聪明的思想的丰富知识宝库。如果你使用我的推荐链接,你将获得我的超级感激和对支持我工作的虚拟击掌。

[## 通过我的推荐链接加入 Medium — Bex T.

独享我所有的 ⚡高级⚡ 内容和 Medium 上的所有内容,无限制地支持我的工作,购买…

ibexorigin.medium.com

图片由我通过 Midjourney 制作。

Python 中 * 和 ** 的 6 种用例

原文:towardsdatascience.com/6-use-cases-in-python-where-and-come-in-handy-530dd9d04875

通过示例进行解释

Soner YıldırımTowards Data Science Soner Yıldırım

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

--

图片由Szabolcs Toth拍摄,发布在Unsplash

如果你曾经浏览过 Python 库的文档,你会不可避免地注意到在多个地方使用了*或**。

那么,*和**到底有什么作用?

我们将学习 * 和 ** 有用的 7 种用例,包括你在文档中看到的情况。

用例最好通过示例进行说明,所以我们直接进入主题。

1. 使用可迭代对象调用函数

假设你定义了一个接受 5 个位置参数的函数,将它们相加并返回总和。

# define the function
def calculate_sum(a, b, c, d, e):
    return a + b + c + d + e

# call the function
result = calculate_sum(1, 4, 3, 2, 6)
print(result)
16

如果传递给函数的参数存储在一个可迭代对象中(例如列表),你可以直接使用列表调用函数,但你需要使用*,如下所示:

# call the function
numbers = [1, 4, 3, 2, 6]

calculate_sum(*numbers)
print(result)
16

如果你尝试调用函数calculate_sum(numbers),Python 会报错,抛出TypeError。*的作用是解包可迭代对象中的值,有时被称为参数解包。

需要注意的是,numbers 列表中的值的数量必须等于函数所需的参数数量,这里是 5。如果不匹配,我们会得到一个TypeError

如果你也想了解更多关于 Pandas 的信息,可以访问我的课程 500 Exercises to Master Python Pandas

2. 使用字典调用函数

这个用例类似于之前的示例。在之前的示例中,函数只有位置参数。在具有关键字参数的函数中,我们仍然可以进行参数解包,但使用的是**而不是*。

# define the function
def calculate_mass(density, length=1, width=1, height=1):
    return density * length * width * height

# call the function
density = 20
mass = calculate_mass(density, 2, 3, 5)
print(mass)
600

这个函数使用物体的密度、长度、宽度和高度来计算质量。假设这些测量值存储在一个 Python 字典中。我们可以直接使用字典来调用函数,如下所示:

# call the function
density = 20

measures = {
    "length": 2,
    "width": 3,
    "height": 5
}

calculate_mass(density, **measures)
600

类似于第一个例子,如果我们尝试以 calculate_mass(density, measures) 的形式调用它,我们会得到一个 TypeError。这是另一个参数解包的例子。

3. 定义一个可以接受任意数量位置参数的函数

你定义一个函数来汇总给定的值,但你不想对传递的值数量施加约束。相反,你希望它具有动态性,能够汇总任意数量的给定值。

下面是我们如何定义这个函数:

# define the function
def calculate_sum(*args):
    result = 0
    for i in args:
        result += i
    return result

*args 表达式将传递给函数的参数打包,使我们可以使用任意数量的参数或可迭代对象来调用函数。

下面是调用这个函数的不同方式:

# calling with 2 values
calculate_sum(3, 4)
7

# calling with a list
my_list = [1, 4, 5, 10, 20]
calculate_sum(*my_list)
40

# calling with a value and a list
my_list = [10, 20, 30]
calculate_sum(5, *my_list)
65

在使用可迭代对象来调用函数的情况下,我们仍然需要在调用函数时加上 *

关于位置参数和关键字参数的说明:

位置参数仅通过名称声明。当函数被调用时,必须提供位置参数的值。否则,我们会得到一个错误。

关键字参数通过名称和默认值来声明。如果我们没有为关键字参数指定值,它将采用默认值。

4. 定义一个可以接受任意数量关键字参数的函数

这一个与之前的用例类似,但我们将创建一个接受关键字参数的函数。

# define the function
def greet(name, **kwargs):
    greeting = f"Hello, {name}.\n"

    if kwargs:
        greeting += "Here are the things I know about you:\n"
        for key, value in kwargs.items():
            greeting += f" {key.title()}: {value}\n"

    return greeting

这个函数根据名字向人们打招呼。如果它被调用时附带了关于这些人的其他关键信息,函数也会打印这些信息。

**kwargs 表达式允许我们在调用函数时传递任意数量的关键字参数。

下面是调用这个函数的不同方式:

# calling with the name (positional argument) only
print(greet("John"))
# output
Hello, John.

# calling with adding some keyword arguments
print(greet("Jane", age=34, job="doctor", hobby="chess"))
# output
Hello, Jane.
Here are the things I know about you:
 Age: 34
 Job: doctor
 Hobby: chess

# calling with adding some keyword arguments as a dictionary
ashley = {
    "age": 27,
    "profession": "athlete",
    "hobby": "reading"
}

print(greet("Ashley", **ashley))
# output
Hello, Ashley.
Here are the things I know about you:
 Age: 27
 Profession: athlete
 Hobby: reading

在将关键字参数作为字典传递时,我们需要在调用函数时加上 **

5. 合并字典

我们可以使用 ** 来合并字典。

ages = {
    "John": 34,
    "Jane": 36
}

new_items = {
    "Matt": 28,
    "Ashley": 24
}

ages = {**ages, **new_items}

print(ages)
{'John': 34, 'Jane': 36, 'Matt': 28, 'Ashley': 24}

在这种情况下,使用 ages = {ages, new_items} 也会引发 TypeError

6. 将值打包成可迭代对象

假设我们有一个包含多个值的列表。我们想要从这个列表中将一个值赋给另一个变量,并将其余的值赋给一个不同的变量。通过下面的例子会更清楚:

first, *others = [3, 5, 1, 10, 24]

print(first)
print(others)
# output
3
[5, 1, 10, 24]

第一个值被分配给一个名为 first 的变量,其余的值被打包到另一个名为 others 的列表中。

我们还可以按照下面的例子来使用它:

first, *others, last = [3, 5, 1, 10, 24]

print(first)
print(others)
print(last)
# output
3
[5, 1, 10]
24

这个用例在处理返回多个值的函数时可能会有所帮助。这里是一个例子:

# define the function
def my_func(a, b, c, d, e):
    sum_1 = a + b
    sum_2 = a + b + c
    sum_3 = a + b + c + d
    sum_4 = a + b + c + d + e
    return sum_1, sum_2, sum_3, sum_4

# call the function
first_sum, *other_sums = my_func(1, 3, 5, 2, 6)

print(first_sum)
print(other_sums)
# output
4
[9, 11, 17]

my_func 函数返回一个包含 4 个值的元组。第一个值分配给一个名为 first_one 的变量,其他值分配给一个名为 other_sums 的列表。

结束语

正如我们在例子中演示的,*** 在 Python 中非常有用。我们可以用它们来进行参数的打包和解包。

这些也在定义和调用函数时使用,以增加更多的灵活性和多功能性。

你可以成为 Medium 会员 以解锁对我所有写作内容的完全访问权限,以及 Medium 上的其他内容。如果你已经是会员,请不要忘记 订阅 ,如果你希望在我发布新文章时收到电子邮件的话。

感谢你的阅读。如果你有任何反馈,请告诉我。

为数据科学团队建立最佳实践的 6 种方法

原文:towardsdatascience.com/6-ways-to-build-best-practices-for-data-science-teams-ca9b83fb269d

为高绩效数据科学团队设定标准

Rebecca VickeryTowards Data Science Rebecca Vickery

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

--

图片由 Marvin Meyer 提供,来源于 Unsplash

数据科学是一个将传统数学和统计学与大规模计算能力以及更现代的机器学习和深度学习技术相结合的领域,从数据中生成见解。

数据科学学科复杂,具有实验性,并且存在很大的不确定性。与软件工程等相关领域类似,数据科学团队需要以受控的方式处理代码开发。然而,除了这些,数据科学家还需要处理不断变化的数据,并执行可重复的实验,例如试验机器学习模型的新特性。

因此,无论你是一个单独工作的数据科学家,还是一个大团队共同工作,制定一套最佳实践标准都非常重要。这些标准将确保:

  • 一个数据科学家可以在稍后的时间重复他们自己的实验、模型或见解。

  • 团队中的其他数据科学家可以重复以上所有内容——如果你在维护生产中的模型,这一点尤其重要。

  • 单个数据科学家和其他团队成员都能在现有工作基础上进行构建,而不是重复相同的实验、代码、模型或分析。

在以下文章中,我将给出一些颇具个人观点的建议,说明如何以及使用什么来设定数据科学团队的最佳实践。然而,每个团队都是不同的,因此你可能需要调整这些建议以满足自己团队的具体需求。

这是我关于数据科学领导力系列文章中的第二篇。第一篇文章链接如下。

学习成为数据科学领导者

1. 代码标准

高质量的代码确保你的团队编写的代码易于他人阅读和理解。这有助于提高团队工作成果的可重现性和可扩展性。

代码应该保持清晰、结构良好,并尽可能模块化。选择共享的编码标准是一个好主意。如果你的团队使用 Python,那么 Google Python Style Guide 可以作为一个好的标准。编码风格可以通过使用如 PylintBlack 的工具,通过 linting 轻松地强制执行,作为开发过程的标准部分。

一个好的代码标准应该包括标准化的命名约定和用于代码文档的 doc 字符串。制定这些标准的目标应始终是使代码尽可能可读和易于理解,并减少团队的心理负担。一个好的编码标准会提高团队的效率,例如,如果代码始终遵循预期的风格,进行同行代码审查的速度会更快。

代码标准可以通过 持续集成 (CI) 检查自动化,作为你的 GitHub 工作流的一部分。

2. 虚拟环境

代码可重现性的一个重要部分是嵌入能够使团队成员编写的代码在任何其他计算机或云环境中运行的过程。

虚拟环境是实现这一点的常用技术。虚拟环境记录了与运行特定项目所需的依赖项(工具)及其版本相关的详细信息。

创建虚拟环境的常用工具包括 Poetry、PipenvConda。虽然它们的工作方式略有不同,但从高层次来看,虚拟环境通过创建和维护一个存储项目的 Python 版本和所有依赖项的文件来工作,然后使用该文件在任何机器上生成一个隔离的环境。

如果团队不能在不同的机器上精确重现相同的环境,那么在缺少原项目创建者的情况下,项目代码很可能无法在没有一些努力的情况下运行。

通常,每个项目有自己的虚拟环境是一个好做法。我建议团队内决定使用一个工具以保持一致性,并方便团队成员重现环境。例如,我自己的团队目前使用 Poetry 来管理我们所有项目的环境。

3. 版本控制

所有数据科学团队应使用版本控制工具,如 Github,以确保所有工作的副本安全地存储在你的笔记本电脑之外,并且可以以受控的方式对现有代码进行更改。

创建拉取请求(PR)流程以促进协作、确保输出质量和分享知识通常是一个好主意。在我看来,PR 应在项目生命周期中定期提出,并且应尽量避免非常大的 PR。尽可能多地获取代码和项目方向的第二意见,以避免大规模的重写或错误,是良好的做法。

一般来说,以下标准应适用于 Github 的使用:

  • 为分支设置标准化的命名约定。

  • 鼓励使用清晰和描述性的提交信息。

  • 为良好的PR 礼仪制定标准。

  • 每天提交代码。

  • 每两周至少提一次 PR,或者如果你的团队使用冲刺周期,则在每个冲刺结束时提 PR。

4. 组织良好的代码

团队中所有项目的一致文件夹结构将使其他团队成员更容易快速阅读和理解任何项目的代码。

网上有许多建议的数据科学文件夹结构的示例。我找到的可能最好的例子是Cookiecutter模板。然而,根据我的经验,文件夹结构在数据科学团队中差异很大,因为使用的具体工具、技术和任务会大大影响标准结构的要求。因此,我的建议是使用类似 Cookiecutter 的工具,但根据团队的需求进行调整。

我非常信奉 KISS(“保持简单,愚蠢”)设计原则,并且会选择最基本的文件夹结构以满足团队的需求。下方展示了我团队用于开发工作的文件夹结构。你可以看到我们选择了一个简单的结构,满足了我们非常特定的需求,但这不一定适用于其他团队。

我团队的文件夹结构。图片来源:作者。

5. 文档

良好的文档实践是确保团队工作可重复性和可复现性的另一种方式。文档应以标准方式记录,并且要详尽到足以确保对项目不熟悉的人能够理解运行任何代码,并复现和理解任何结果。

一般来说,我会在数据科学团队的最佳实践中包含以下类型的文档。

代码文档: 所有代码应附带理解和运行所需的最少文档。这包括为函数添加文档字符串,在项目文件夹或仓库中包含 README.md 文件,以及为存储在 Jupyter Notebooks 中的代码添加注释。

产品文档: 产品文档涉及与项目的开发、设计和结果相关的信息。通常,这些文档可能采用 Google 文档、Confluence 页面或 Miro 白板的形式。这类文档的最终用户通常是非技术性利益相关者和产品经理。

6. 组织良好的 notebooks

Jupyter Notebooks 的特性可能导致代码混乱、难以阅读和不可重现。因此,在使用 notebooks 时,遵循本文中已经概述的最佳实践,如版本控制和良好的代码风格,是至关重要的。然而,也需要添加一些与 notebook 使用特别相关的额外最佳实践。

以下是包含在你的 notebook 最佳实践中的一些良好标准列表。

  • 利用 notebook 中丰富的代码注释能力,包含标题、章节标题、注释、图像和图表,以使代码和结果尽可能易于理解。

  • 将所有导入语句放在 notebook 顶部的单个单元格中。

  • 确保数据源明确。这可能意味着在 notebook 中包含指向云存储桶或 SQL 查询的链接。

  • 保持 notebook 简单,尽量减少代码行数。在合适的地方将复杂的代码抽象为函数和模块,并确保删除任何冗余的代码。

由于其复杂和实验的特性,数据科学是一个在标准化方法和团队最佳实践中获益匪浅的领域。上述列出的所有最佳实践旨在确保团队项目的可重复性、可扩展性和可复现性。

此外,尽管最佳实践需要一些时间和精力来嵌入和遵守,但从长远来看,它们将节省大量时间和精力,避免重新运行和理解项目或重复已经完成的工作。

我上面列出的最佳实践并非详尽无遗,根据团队使用的工作类型和工具,你的最佳实践可能会有所不同。本文旨在作为最佳实践的入门指南,并应提供一些灵感,以帮助你开始为团队定义和设定自己的标准。

如果你想深入了解数据科学团队的最佳实践,我在本文底部列出了有用的进一步阅读资料。

感谢阅读!

有用的资源

IBM 数据科学最佳实践

Google Cloud 的最佳实践:提升任何使用 Jupyter Notebooks 开发者的工作体验

用于可重复 Jupyter Notebooks 的 4 个工具

组织数据科学项目的方案

从 Pandas 切换到雷电般快速的 Polars 并永不回头的 7 个简单步骤

原文:towardsdatascience.com/7-easy-steps-to-switch-from-pandas-to-lightning-fast-polars-and-never-return-b14c66fc85b9

最常见的 Pandas 操作的速查表翻译为 Polars

Bex T.Towards Data Science Bex T.

·发布于数据科学前沿 ·阅读时间 9 分钟·2023 年 4 月 3 日

--

图片由作者通过 Midjourney 提供

是时候说再见了!

Pandas 可以做任何事。几乎任何事。但(这真的是我无数次希望它可以有所不同的)它缺乏速度。Pandas 根本无法跟上今天数据集规模和复杂度增长的速度。

Pandas 的作者 Wes McKinney 表示,当他编写 Pandas 时,他有一个针对其库的经验法则:

拥有比数据集大小多 5 到 10 倍的 RAM。

也许你曾经对引入 Iris 数据集时的这一规则视而不见,但今天情况有所不同。你根本无法在 RAM坚决停留在 64GB 的情况下加载 100GB 的数据集(这在现代社会中已经很常见)。

当然,也有像 Dask 这样的优秀替代方案。但 Dask 并没有实现新的功能。它只是将现有的 Pandas 语法扩展到多个进程(线程)上,忽略了底层的性能和内存问题。

它将 Pandas 视为黑箱。请原谅我这样说,但这几乎就像是在猪身上涂口红一样。

Polars,本文的重点,是从头开始用 Rust 编写的,旨在修复 Pandas 的所有不足。在我上一篇文章中,我们已经看到它比即将推出的 Pandas 2.0(配备 PyArrow 后端)更快。

虽然那篇文章重点关注速度优势,这篇文章则更多地关注语法和功能,并展示了如何在七个简单步骤中从 Pandas 切换到 Polars,也许永远不会再切换回来。

0. 读写数据

尽管速度极其缓慢,CSV 仍然是存储数据的最流行文件格式之一。所以,让我们从 Polars 的read_csv函数开始吧。

除了明显的速度优势外,它只在参数数量(Pandas 的 read_csv 有 49 个。是的,我数过)和语法上与 Pandas 有所不同。

作者提供的图片

混淆的参数名称不应该是问题,因为大多数现代 IDE 都具有自动补全或弹出文档功能(JupyterLab 上的Shift + Tab,谢谢 🙏)。

如果你不知道的话,dtype 参数防止 Pandas 设置自动数据类型,并允许用户设置自定义数据类型,如 cutstring 类型或日期类型列的 datetime

你可以在 Polars 中使用相同的行为,使用 dtypes(注意是‘s’),虽然它不允许通过字符串设置类型。你必须提供 Python 内建类型或 Polars 的类型,例如 pl.Booleanpl.Categoricalpl.DateTimepl.Int64pl.Null(表示缺失值)。你可以通过调用 dir(pl) 查看完整列表。

读取和写入 Parquet 文件,这比 CSV 文件更快、更节省内存,Polars 通过 read_parquetwrite_parquet 函数也支持这些操作。

作者提供的图片

1. 创建 Series 和 DataFrames

你并不总是从文件中读取数据。像在 Pandas 中一样,你可以从头创建 DataFrames 和 Series,语法几乎相同:

作者提供的图片

Polars DataFrames 也有许多与 Pandas 几乎相同名称和行为的方法。打个招呼吧:

  • [apply](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.apply.html#polars.DataFrame.apply) - 对 DataFrame 的每一行应用自定义用户定义的函数

  • [corr](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.corr.html#polars.DataFrame.corr) - 相关性矩阵

  • [describe](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.describe.html#polars.DataFrame.describe) - 汇总统计,五数概括

  • [drop](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.drop.html#polars.DataFrame.drop) - 从 DataFrame 中删除列

  • [explode](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.explode.html#polars.DataFrame.explode) - 将给定列解包为长格式(当单元格包含多个值如 [1, 2, 3] 时)

  • [head](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.head.html#polars.DataFrame.head)[tail](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.tail.html#polars.DataFrame.tail)[sample(n)](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.sample.html#polars.DataFrame.sample) - 获取 DataFrame 的不同视图(顶部、底部、随机)

  • [iter_rows](https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.iter_rows.html#polars.DataFrame.iter_rows) - 返回一个 DataFrame 行的迭代器,包含 Python 原生值

  • maxmeanmediansumstd 和常见的数学和统计函数。

等等。查看 这个页面 以获取 Polars 中 DataFrame 方法的完整列表。

2. 理解 Polars 中的表达式

这是一个查询引擎,人类。 — Midjourney

Polars 的核心是其 查询引擎,它运行用户定义的 表达式。查询引擎和表达式是 Polars 极快性能的两个关键组件,正如 Polars 用户指南所说的,“令人尴尬的并行”。

你可能会惊讶于 Polars 表达式与 SQL 的相似程度,同时保持与熟悉的 Pandas 语法的紧密联系。

像 SQL 查询一样,你可以为以下内容编写表达式:

  • 从现有列创建新列

  • 在某些转换后获取数据视图

  • 汇总统计

  • 处理和清理数据

  • Groupby 语句

等等。

df.filter(pl.col('column') == 'some_value')

在上面的查询中,表达式是 pl.col('column)' == 'some_value',这正如你所猜测的,过滤 DataFrame 以获得 column 等于 some_value 的行。

当你单独运行这个表达式时,你不会得到像在 Pandas 中那样的布尔 Series:

type(pl.col("column") == "some_value")
polars.expr.expr.Expr

这是因为表达式只在 上下文 中进行评估。Polars 中有三种广泛的上下文:

  1. 选择数据 — 在 select 上下文中,表达式应用于列,并且必须在结果中产生相同长度的列。这种行为应当对你来说很熟悉。filter 函数也与这个上下文相关联。

  2. 分组数据 — 在 groupby 上下文中,表达式在组上工作,结果可能具有任何长度,因为一个组可以有多个成员。

  3. 添加新列 — 在这种情况下,表达式用于从头开始或从现有列创建新列。

让我们详细看看每个上下文。

3. 选择数据

Pandas 的括号表示法让位于 Polars 中用于选择列的表达式。

要选择单个列,你可以在 select 中使用它们的字面名称,或使用推荐的 pl.col 函数来引用列。

要选择多个列,你可以在 pl.col 中列出列名,用逗号分隔,或在 select 中列出 pl.col 引用的列表。我们稍后会看到这些语法之间的区别。

图片由作者提供

Polars 包含一些 Pandas 中未完全提供的选择数据功能。例如,你可以使用 exclude 排除选择中的列:

df.select(pl.exclude("price")).head()

图片由作者提供

或者使用 ^$ 字符之间的正则表达式。下面,我们选择所有以字母 c 开头的列:

df.select(pl.col("^c.+$")).head()

图片由作者提供

你还可以根据数据类型对子集 DataFrame,这可能让你想起 Pandas 中的 select_dtypes(左侧):

图片由作者提供

要选择所有数值列,我们在 pl.col 中使用了 Int64Float64 类型。

4. 数据过滤

你可以使用 filter 函数通过布尔索引对 DataFrames 进行子集化。例如,在列上使用 is_between 函数可以创建一个表达式来筛选范围内的数值列。

图片由作者提供

你可以使用熟悉的布尔运算符 & (AND) 和 | (OR) 结合多个 条件表达式。在下面的示例中,我们选择 color 列为 'E' 或 'J' 并且钻石的 price 低于 500 的行:

图片由作者提供

另外,注意我们在右侧如何使用 Polars 中的 is_in

5. 创建新列

你可以在 with_columns 上下文中创建新列。在下面的示例中,new_col 使用 pl.col('price') ** 2 定义,并通过别名给新列命名,就像 SQL 中的 as 关键字一样。

图片由作者提供

在第二个示例中,我们结合了两个列(尽管这没有意义),演示了如何将整数和字符串列与 Polars 结合使用。你可以在使用 pl.col 引用的列上使用任何本地 Python 或第三方函数和运算符。

如果你想将新列插入到 DataFrame 中,你必须覆盖原始的 df 变量。

顺便提一下,Polars 中的字符串列具有熟悉的 .str 接口,用于特殊的文本操作函数,如 containslengths。完整列表请参见 这里。还有用于专业分类、时间和数组函数的 [.cat](https://pola-rs.github.io/polars/py-polars/html/reference/series/categories.html)[.dt](https://pola-rs.github.io/polars/py-polars/html/reference/series/temporal.html)[.arr](https://pola-rs.github.io/polars/py-polars/html/reference/series/list.html) 接口。

6. 分组操作

我不认为我们可以不提及 Groupby 操作:

图片由作者提供

使用 Polars 的 groupby 函数时,务必包含 maintain_order=True 以确保组不会随机显示。此外,与 Pandas 不同,groupby(col_name) 表达式仅作用于给定列。要根据 col_name 对所有列进行分组,你必须使用聚合上下文。其语法如下:

df.groupby(
    "cut", maintain_order=True
).agg(pl.col("*").count())

groupby 上下文之后,你将链式聚合上下文,并指定上下文影响哪些列。然后在结果上链式调用任何函数,例如 count

这是另一个示例,按钻石切割质量分组,并返回每组的平均数值:

要了解更多关于 Polars 中高级 groupby 表达式的信息,请阅读这里

7. Polars 中的懒惰 API

Polars 最棒的功能之一是其懒惰 API。在该 API 中,查询不是逐行执行的,而是由查询引擎端到端处理的。

这是查询优化和令人尴尬的并行魔法发生的地方。你可以仅用两个关键字将任何在急切模式下编写的表达式转换为懒惰模式:

import polars as pl

df = pl.read_csv("data/diamonds.csv")

query = df.lazy().filter(
    pl.col("cut") == "Ideal"
)

type(query)
polars.lazyframe.frame.LazyFrame

当你在链式表达式之前添加 lazy() 函数时,DataFrame 会变成 LazyFrame。此时,查询尚未执行,你可以链式添加更多表达式。一旦准备好,你可以调用 collect() 来获取结果:

query.collect().head()

图片由作者提供

虽然它在急切模式下已经很快,但懒惰模式为查询引擎提供了额外的(是的,三倍的 x)动力。

如果你希望将懒惰 API 设为默认,可以在读取数据时使用 scan_* 函数,而不是 read_*

df = pl.scan_csv("data/diamonds.csv")

q1 = df.filter(
    pl.col("cut") == "Ideal"
)

q1.collect().head()

这样,你就能避免每次都编写 lazy() 函数。

如果你处理的数据集对你的内存构成威胁,你可以使用流式处理,使 Polars 分批处理数据。这个功能可以通过在collect中设置streaming=True来启用。更多关于这一优秀功能的信息,请参见此页面

结论

Polars 可能很新(我是说,它刚刚出生不久),但已经非常受欢迎。在开源标准中,它是明星。只需看看它的竞争对手:

  1. Pandas,发布于 2011 年,拥有 37.5k 个 GitHub 星标。

  2. Apache Spark,发布于 2014 年,拥有 26.8k 个星标。

  3. Vaex,发布于 2017 年,拥有 7.9k 个 GitHub 星标。

  4. Dask,发布于 2015 年,拥有 10.9k 个星标。

  5. Apache Arrow,发布于 2016 年,拥有 11.4k 个星标。

相比之下,Polars 于 2020 年发布,已经积累了 15.9k 个星标,接近其长期顶级竞争对手的一半。

这应该能给你一个大概的了解风向如何变化。虽然 Pandas 2.0 发布后情况可能会有所变化,但我认为 Polars 已经在给 Pandas 带来不小的竞争。

喜欢这篇文章吗?说实话,它那奇特的写作风格呢?想象一下,如果你能访问到更多类似的文章,全部由一位才华横溢、迷人而风趣的作者(顺便说一下,就是我 :)。

只需 4.99$的会员费用,你将不仅能访问我的故事,还能获取 Medium 上最优秀的思想者们的宝贵知识。如果你使用我的推荐链接,你将获得我超级 nova 的感激之情和一记虚拟的击掌,以支持我的工作。

[## 通过我的推荐链接加入 Medium — Bex T.

获得对我所有⚡优质⚡内容的独家访问权限,无限制地浏览 Medium。通过购买我提供的服务来支持我的工作……

ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----b14c66fc85b9--------------------------------)

使用 Python Pandas 掌握分类数据操作的 7 个示例

原文:towardsdatascience.com/7-examples-to-master-categorical-data-operations-with-python-pandas-51cdcb0228ba

在处理低基数类别特征时使用类别数据类型

Soner YıldırımTowards Data Science Soner Yıldırım

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

--

(作者创建的图片)

类别变量可以从有限数量的值中取值,这些值通常是固定的。以下是一些类别变量的例子:

  • 英语水平指标(A1、A2、B1、B2、C1、C2)

  • 一个体的血型(A、B、AB、O)

  • 人口统计信息,如种族和性别

  • 教育水平

Pandas 提供了一种专用的分类变量数据类型(categoryCategoricalDtype)。尽管这样的数据也可以用objectstring数据类型存储,但使用category数据类型有几个优点。我们将了解这些优点,但首先让我们开始学习如何处理分类数据。

当我们用文本数据创建 Series 或 DataFrame 时,其数据类型默认为object。要使用category数据类型,我们需要明确地定义它。

import pandas as pd

# create Series
blood_type = pd.Series(["A", "B", "AB", "0"])

print(blood_type)
# output
0     A
1     B
2    AB
3     0
dtype: object

# create Series with category data type
blood_type = pd.Series(["A", "B", "AB", "0"], dtype="category")

print(blood_type)
# output
0     A
1     B
2    AB
3     0
dtype: category
Categories (4, object): ['0', 'A', 'AB', 'B']

尽管值相同,但正如你在打印 Series 时通过dtype所示,数据类型不同。

我们将通过 7 组示例来学习以下主题:

  1. DataFrames 中的类别数据类型

  2. 类别

  3. 添加和更新值

  4. 添加和删除类别

  5. 类别之间的顺序

  6. 重命名类别

  7. 使用类别数据类型的优点

示例 1 — DataFrames 中的类别数据类型

我们可以在创建 Series 或 DataFrame 时声明category数据类型,如上所述。我们也可以使用astype函数将其转换为category

在下面的代码片段中,我们首先创建一个包含两个object数据类型的列的 DataFrame。然后,我们将blood_type列的数据类型更改为category。记住,DataFrame 的每一列都是一个 Series。

# create a DataFrame with two columns
df = pd.DataFrame(
    {
        "name": ["Jane", "John", "Ashley", "Matt"],
        "blood_type": ["A", "B", "AB", "0"]
    }
)

# check the data types
df.dtypes
# output
name          object
blood_type    object
dtype: object

# convert the blood_type column to category
df["blood_type"] = df["blood_type"].astype("category")

# check the data types again
df.dtypes
# output
name            object
blood_type    category
dtype: object

示例 2 — 类别

category 数据类型的 Pandas Series 是通过类别来定义的。默认情况下,类别是 Series 中的唯一值。

# create Series with category dtype
brands = pd.Series(["Ford", "Toyota", "BMW"], dtype="category")

print(brands)
# output
0      Ford
1    Toyota
2       BMW
dtype: category
Categories (3, object): ['BMW', 'Ford', 'Toyota']

打印品牌时显示类别。我们也可以使用通过 cat 访问器提供的 categories 方法来提取它们。

brands.cat.categories

# output
Index(['BMW', 'Ford', 'Toyota'], dtype='object')

它返回类别的索引。

我们还可以在创建 Series 时定义类别,具体方法如下:

# create Series with category data type
brands = pd.Series(
    pd.Categorical(
        ["Ford", "Toyota", "BMW"], 
        categories=["Ford", "Toyota", "BMW", "Honda"]
    )
)

print(brands)
# output
0      Ford
1    Toyota
2       BMW
dtype: category
Categories (4, object): ['Ford', 'Toyota', 'BMW', 'Honda']

当前 Series 中不存在值“本田”,但它可以被添加,因为它列在类别中。

示例 3 — 添加和更新值

要在 category 数据类型的 Series 中添加新值或替换现有值,我们应该从定义的类别中选择一个值。否则,Pandas 会将 Series 的数据类型更改为 object

# create a Series with category data type
brands = pd.Series(["Ford", "Toyota", "BMW"], dtype="category")

print(brands)
# output
0      Ford
1    Toyota
2       BMW
dtype: category
Categories (3, object): ['BMW', 'Ford', 'Toyota']

# Add a new item of a different category
brands[3] = "Honda"

print(brands)
# output
0      Ford
1    Toyota
2       BMW
3     Honda
dtype: object

当我们添加了新的项“本田”时,它不在列出的类别中,最终得到的 Series 的数据类型为 object

如果我们尝试将现有值更改为不同于现有类别的值,Pandas 会引发类型错误。

# create a Series with category data type
brands = pd.Series(["Ford", "Toyota", "BMW"], dtype="category")

# replace the third value with Honda
brands[2] = "Honda"

# output
TypeError: Cannot setitem on a Categorical with a new category (Honda), set the categories first

解决这个问题的方法有很多。例如,我们可以在使用 Series 之前将“本田”作为新类别添加。

# add Honda as a category
brands = brands.cat.add_categories("Honda")

# replace the third value with Honda
brands[2] = "Honda"

print(brands)

# output
0      Ford
1    Toyota
2     Honda
dtype: category
Categories (4, object): ['BMW', 'Ford', 'Toyota', 'Honda'] 

示例 4—添加和删除类别

我们可以使用 Python 列表一次添加多个类别。

# create Series with category data type
sizes = pd.Series(["S", "M", "L"], dtype="category")

# add two new categories
sizes = sizes.cat.add_categories(["XS", "XL"])

print(sizes)

# output
0    S
1    M
2    L
dtype: category
Categories (5, object): ['L', 'M', 'S', 'XS', 'XL']

就像我们可以添加新类别一样,也可以删除现有类别。

# create Series with category data type
sizes = pd.Series(["S", "M", "L", "XL", "XXL"], dtype="category")

# remove XL and XXL categories
sizes = sizes.cat.remove_categories(["XL", "XXL"])

print(sizes)

# output
0      S
1      M
2      L
3    NaN
4    NaN
dtype: category
Categories (3, object): ['L', 'M', 'S']

需要注意的是,如果 Series 包含属于已移除类别的值(即不再存在的类别),这些值将变为缺失值(即 NaN)。

我们可以使用 categories 方法从 Series 中提取现有类别。

# create Series with category data type
sizes = pd.Series(["S", "M", "M", "L", "L", "S"], dtype="category")

# extract categories
sizes.cat.categories 

# output
Index(['L', 'M', 'S'], dtype='object')

# extract categories as a list
list(sizes.cat.categories)

# output
['L', 'M', 'S']

示例 5—类别之间的顺序

在某些情况下,类别之间有顺序(例如 S < M < L)。有不同的方法来强制执行这种顺序。

一种选择是使用 as_ordered 函数为现有的分类数据 Series 添加顺序。

# create Series with category data type
sizes = pd.Series(["L", "S", "XL", "M", "L", "S"], dtype="category")

# convert it to ordered
sizes = sizes.cat.as_ordered()

print(sizes)

# output
0     L
1     S
2    XL
3     M
4     L
5     S
dtype: category
Categories (4, object): ['L' < 'M' < 'S' < 'XL']

我们现在看到类别之间有顺序,但顺序错误。Pandas 为字符串数据分配字母顺序,这实际上是有意义的。我们可以通过重新排序类别来修复这个问题(请查看下一个示例)。

上一个示例中的 sizes Series 有排序的类别,但顺序错误。让我们使用 reorder_categories 方法来修复它。

# convert it to ordered
sizes = sizes.cat.reorder_categories(["S", "M", "L", "XL"])

print(sizes)

# output
0     L
1     S
2    XL
3     M
4     L
5     S
dtype: category
Categories (4, object): ['S' < 'M' < 'L' < 'XL']

我们将所需的类别顺序写在 Python 列表中,并将其传递给 reorder_categories 方法。

要从类别中移除顺序,我们可以使用 as_unordered 方法。让我们将其应用于之前示例中创建的 sizes Series。

# convert it to unordered
sizes = sizes.cat.as_unordered()

print(sizes)

# output
0     L
1     S
2    XL
3     M
4     L
5     S
dtype: category
Categories (4, object): ['L', 'M', 'S', 'XL']

在创建 Series 时也可以通过使用 ordered 参数来强制执行顺序。

# create Series with category data type
divisions = pd.Series(pd.Categorical(

    values=["C", "C", "A", "B", "A", "C", "A"], 
    categories=["C", "B", "A"], 
    ordered=True

))

print(divisions)

# output
0    C
1    C
2    A
3    B
4    A
5    C
6    A
dtype: category
Categories (3, object): ['C' < 'B' < 'A']

顺序是根据我们写入类别的顺序来确定的(在此示例中为 C、B、A)。

示例 6—重命名类别

如果需要重命名类别,我们可以使用 rename_categories 方法。

在之前的示例中,我们创建了一个名为“division”的 Series,包含类别 C、B 和 A。让我们重命名这些类别。

# rename the categories
divisions = divisions.cat.rename_categories(["group C", "group B", "group A"])

print(divisions)

# output
0    group C
1    group C
2    group A
3    group B
4    group A
5    group C
6    group A
dtype: category
Categories (3, object): ['group C' < 'group B' < 'group A']

正如我们在输出中看到的,重命名类别也会更新 Series 中的值。

示例 7——使用类别数据类型的优势

Pandas 的主要数据结构是 DataFrame,它是一个带有标签的二维数据结构。DataFrame 中的每一列也是一个 Series 对象。因此,我们可以很容易地在 DataFrame 中使用类别数据类型。

在这个例子中,我们将创建一个示例 DataFrame,然后通过将现有列的数据类型更改为类别类型来添加一个新列。

import numpy as np

# create a DataFrame with 100000 rows
cars = pd.DataFrame({

    "id": np.arange(1, 100001),
    "brand": ["Ford", "Toyota", "BMW", "Tesla"] * 25000,
    "price": np.random.randint(10000, 20000, size=100000)

})

# add a brand_categorical column
cars["brand_categorical"] = cars["brand"].astype("category")

# check the data types
cars.dtypes

# output
id                      int64
brand                  object
price                   int64
brand_categorical    category
dtype: object

我们创建的 DataFrame 如下所示。brandbrand_categorical 列存储了相同的数据,但数据类型不同。

车辆 DataFrame 的前 5 行(图像来源:作者)

使用类别数据类型而不是对象或字符串数据类型的目的是什么?数据本身还是一样的。

答案是内存使用情况。特别是当不同值的数量远低于总值数量(低基数)时,使用类别数据类型而不是对象类型可以节省大量的内存空间。

让我们通过计算车辆 DataFrame 中列的内存使用量来确认。

# check the data types
cars.memory_usage()

# output
Index                   132
id                   800000
brand                800000
price                800000
brand_categorical    100204
dtype: int64

它计算的是以字节为单位的内存使用量。使用类别数据类型相比于对象数据类型,可以节省 8 倍的内存。当我们处理更大的数据集(例如,数百万行)时,这种差异更为重要。

结语

类别数据类型相比其他基于字符串的数据类型相对较少见。原因可能是我们通常在将字符串数据用于机器学习模型之前会对其进行编码。然而,即使在数据清理和准备过程中,类别数据类型也提供了重要的优势。因此,如果一个基于字符串的变量相对于总值包含的不同值较少,我强烈建议使用类别数据类型。

感谢阅读。如有任何反馈,请告诉我。

AI 并没有威胁我们的工作!这里有 7 个值得关注的前沿职位,预计到 2030 年会崭露头角

原文:towardsdatascience.com/7-kinds-of-ai-jobs-created-in-the-future-b87191452f99

这篇文章突出了 AI 将在未来几年带来的创新角色。

Ken JeeTowards Data Science Ken Jee

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

--

图片来源于 Lucas 在 Pixabay

目录

  • 职位 #1: AI 安全分析师与 AI 防御工程师

  • 职位 #2: AI 数据工程师与数据策展人

  • 职位 #3: AI 数据合规专家

  • 职位 #4: AI 政策监管者

  • 职位 #5: 首席 AI 伦理官和定量 AI 伦理学家

  • 职位 #6: Prompt 工程师

  • 职位 #7: AI 体验工程师

  • 结论

ChatGPT 已经引起了世界的轰动。它已经拥有超过 1 亿用户,是有史以来增长最快的产品。

作者提供的图片

毫无疑问,它将颠覆我们的工作和操作方式。它已经彻底改变了我作为数据科学家的工作。

其他公司也没有落后于 OpenAI。来自 NVIDIA、Midjourney 以及其他大型科技公司、初创公司和开源社区的生成式 AI 技术每天都在取得令人印象深刻的进展。

在未来几年中,工作环境将经历重大变化。AI 将取代许多工作,但我认为它也会创造一些真正引人入胜的职位。现在正是开始思考未来雇主可能需要的职位和技能的时候。

职位 #1: AI 安全分析师与 AI 防御工程师

虽然令人恐惧,但 AI 已经被用来威胁和诈骗他人。语音克隆让人们能够伪装成亲人以索要和窃取金钱。全球安全威胁带来了更大的问题。

我预计政府和私人企业将大力投资于 AI 威胁的安全。例如,美国政府在过去一年中将其非机密 AI 预算增加了 26%,这似乎符合这一趋势。我预计机密预算会更高。

作者提供的图片

新角色 AI 安全分析师或 AI 防御工程师专注于分析和打击对公司和个人的 AI 威胁

作者提供的图片

一些所需的技能和资格

  • 了解 AI 技术和应用,如语音克隆、深度伪造、自然语言处理、计算机视觉等。

  • 能够识别和分析潜在的 AI 威胁和漏洞,如欺骗、网络钓鱼、恶意软件、数据泄露等。

  • 能够设计和实施 AI 防御策略和解决方案,如加密、认证、验证、监控等。

  • 熟悉安全标准和最佳实践,如 ISO 27001、NIST 框架等。

  • 具有安全工具和框架的经验,如 TensorFlow Privacy、PySyft、PyTorch Crypten 等。

职位 #2: AI 数据工程师与数据策展人

在 Sam Altman 与 Lex Fridman 的采访中,Altman 强调了数据作为 GPT-4 改进的重要贡献者。寻找新的数据源来训练模型将对这些新技术的发展至关重要。

寻找数据是一回事,但还需要有人将这些数据结构化,以便用于训练这些大型模型。这就是 AI 数据工程师和数据策展人 发挥作用的地方。他们需要找到数据,并使其适用于 AI 模型。

需要注意的是,为这些大型语言模型收集和存储的数据通常与传统商业用途的数据收集和存储方式有很大不同。这是因为数据的多样性、体积和质量等因素差异。学习处理大型模型数据的不同技能和工具可以使某人领先于行业。

作者提供的图片

我认为这一角色将远远超出仅仅是像 GPT 这样的大型 AI 模型训练公司。随着 AI 的民主化,所有公司都希望为他们自己的用例,如客户服务和文档,训练开源的大型语言模型。

作者提供的图片

我不认为像这样的数据角色需求会很快减缓。

一些所需的技能和资格

  • 了解数据源和格式,如文本、音频、视频、图像等。

  • 能够收集、清理、标记和组织 AI 模型的数据,如使用数据管道、标注工具、数据质量检查等。

  • 能够存储和管理大规模数据集,例如使用云平台、数据库、数据仓库、数据湖等。

  • 能够理解和应用数据伦理和隐私原则,例如通用数据保护条例(GDPR)、消费者隐私法、数据匿名化、数据治理等。

  • 熟悉人工智能技术和框架,例如自然语言处理、计算机视觉、TensorFlow、PyTorch 等。

职位#3: 人工智能数据合规专家

虽然一切都围绕数据,但通常谁拥有数据仍然不清楚。在全球范围内,数据隐私法律和可用于训练大模型的数据也存在差异。公司可能需要那些对数据立法模糊地带感到舒适的律师,因为这正是这些公司运营的领域。

图片由作者提供

OpenAI、Microsoft 和 GitHub 目前因涉嫌抓取许可代码而面临诉讼。还有人猜测许多这些模型是基于盗版书籍和其他内容进行训练的。

这些模型通常还会通过用户输入的文本或图像进行训练。这是一个额外的领域,情况变得有些模糊。

随着这些问题逐渐引起重视,公司将需要具有法律背景以及一些数据专业知识的人员,担任人工智能数据合规专家,以帮助他们应对这些挑战并降低潜在法律问题的风险

图片由作者提供

一些必需的技能和资格

  • 了解数据隐私法律和法规,例如 GDPR、消费者隐私法等。

  • 能够进行数据保护影响评估(DPIAs),并识别在人工智能系统中使用个人数据的潜在风险和缓解措施。

  • 能够管理和映射人工智能合规的权威规则和要求,并确保与业务目标和控制的对齐。

  • 能够监控和审计人工智能系统的性能和行为,确保其遵守伦理原则和标准。

  • 能够与利益相关者沟通和协作,如数据科学家、工程师、法律监管者、客户等。

职位#4: 人工智能政策监管员

当公司努力保持领先于法律或避免法律问题时,其他公司将需要努力弄清楚如何立法那些我们前所未见的人工智能系统。

与 ChatGPT 的进步一起,人工智能相关法案的通过数量每年都在增长,我认为我们将会看到新的立法爆炸性增长。

图片由作者提供,灵感来自于斯坦福大学

我认为大多数 AI 立法将由智库、大学和特殊利益集团发起,但最终我们将有充分信息的个人与州和地方政府合作,以规范和监督 AI 的使用。

我会称这些人AI 政策监管者。他们将帮助制定有关 AI 的立法,并在其管辖范围内执行标准实践

一些必要的技能和资格

  • AI 技术和应用及其经济和社会影响的知识。

  • 能够使用 AI 工具和方法生成政策制定和评估的见解和预测。

  • 能够与各种利益相关者(如研究人员、行业、民间社会和其他政府)进行沟通和合作。

  • 能够平衡 AI 监管的风险和机会,并确保与伦理原则和人权的一致性。

  • 能够监控和强制执行 AI 系统和用户遵守相关法律和标准。

职位 #5: 首席 AI 伦理官及定量 AI 伦理学家

尽管 AI 数据合规专家将专注于在围绕 AI 的设计选择之后避免法律问题,但仍需要有人专注于是否应该做出该设计选择。所有发布的 AI 模型都应与人进行交互。我们希望这些互动能为这些人的生活带来积极的结果。

我认为 AI 伦理将进入 C-suite。我预计公司将在不久的将来任命首席 AI 伦理官或类似职位。

图片来源:作者

这个职位的工作是尽可能减少生产模型和过程中的偏见。还要确保模型的结果对相关方是积极和公平的

我认为这个角色会在 C-suite 中,因为这个角色涉及到大量的限制。如果这个人没有高的职位或在组织中的尊重,解雇他们并聘用一个会放任事情发展的人会比较容易。实际上,这种情况可能仍会发生。

一些必要的技能和资格

  • AI 技术及其对社会和个人的影响的技术知识。

  • 了解现有和新兴与 AI 伦理相关的法律和标准的监管知识。

  • 具有商业头脑和关于 AI 应用的特定领域及背景的行业知识。

  • 沟通技巧以及跨组织边界和与不同利益相关者合作的能力。

  • 具有前瞻性和预测能力,以预见 AI 创新的潜在后果和机会。

首席 AI 伦理官的最大武器将是定量 AI 伦理学家。他们的工作是分析模型中的偏见水平并衡量对受影响群体的影响

我认为这个角色将标志着伦理观念在组织中的重大变化。对于创建这些模型的人来说,需要数据来推动变革。尽可能量化伦理将提高认可的速度。

一些必需的技能和资格

  • 对 AI 技术、系统、算法和工具的技术知识。

  • 对指导 AI 开发和使用的理论和原则的伦理理解。

  • 分析和批判性思维技能,以评估复杂的伦理问题并提出解决方案。

  • 沟通技能和向不同听众解释和辩护伦理决策的能力。

  • 统计和数学技能,以测量和量化 AI 模型及结果中的偏见和公平性水平。

职位 #6: 提示工程师

作为数据科学家,我每天使用 ChatGPT。老实说,我几乎不再写代码了。我大部分时间都在编写和修订提示,以改进 ChatGPT 编写的代码。

在我看来,未来大量的知识工作将依赖于提示 即我们创建输入以要求 AI 生成所需结果的方式。

例如,我可以提示 ChatGPT 成为数据科学教师,并向其提问。

作者提供的图片

我还可以将它提示成为一个著名的艺术评论家,利用我对图片的描述来改善它们,以便输入到像 Midjourney 这样的文本到图像模型中。

创造脚本以将这些模型塑造成你想要的样子或执行你希望的操作是一门艺术。

我认为特定行业会有提示工程师的市场。而且有些人已经在尝试聘请这些工程师,薪资超过 $300,000。

作者提供的图片

一些必需的技能和资格

  • 批判性思维和解决问题的技能,以创建有效的提示,向 AI 模型传达人的意图。

  • 数学和分析技能,以理解和操作数学公式及数据。

  • 沟通和写作技能,以使用自然语言编写清晰简明的提示。

  • 关注细节和准确性,以避免提示中的错误和模糊性。

  • 多才多艺和适应能力,以应对不同的 AI 系统和领域。

  • 团队合作和协作技能,以协调其他提示工程师和相关方。

  • 编码和编程技能,以使用各种工具和框架进行提示工程。

  • A/B 测试和实验技能,以评估和优化提示的性能。

职位 #7: AI 体验工程师

目前,我们通过文本、图片以及有时通过语音直接与 AI 互动。我预计随着 AI 的不断发展,这种情况将迅速变化。公司将希望让 AI 融入我们的生活。这个工作的主要部分由设计师或用户体验工程师完成。

如果人们不断使用这些工具,公司将希望它们能提供最佳的体验。这可能是选择一种产品而非另一种产品的关键因素。

我个人在使用 ChatGPT 与 Bard 相比时获得了这种体验。我发现使用 ChatGPT 的体验要好得多:我喜欢它的美学,更喜欢它将所有文本写在屏幕上的方式,还喜欢我可以将其设置为暗模式。即使输出相同,我通常也会更喜欢 ChatGPT。

ChatGPT(左图)和 Bard(右图)之间用户体验的比较。图片由作者提供

我认为公司将大量投资于新的AI 体验工程师,以理解和开发消费者使用其产品的方式,使产品脱颖而出

一些必要的技能和资格

  • 具有用户研究、用户测试和用户反馈的经验,以了解用户的需求、偏好和痛点。

  • 能够创建用户角色、用户旅程、用户流程、线框图、模型图和原型,以设计和传达用户体验。

  • 了解设计原则、模式和最佳实践,以创建直观、可访问和引人入胜的用户界面。

  • 熟练使用设计工具,如 Sketch、Figma、Adobe XD 等,以创建和迭代产品的视觉设计。

  • 熟悉 HTML、CSS 和 JavaScript 等编程语言,以实施产品的前端设计并与开发人员合作。

  • 理解 AI 概念、框架和工具,以将 AI 功能集成到产品中。

  • 了解 AI 伦理和原则,以确保 AI 的负责任和公平使用。

结论

生成式 AI 的兴起将取代许多传统工作,但它也承诺带来一系列令人兴奋的新就业前景。这一影响已经显而易见,显著改变了各行业专业人士的工作环境。随着这一波 AI 创新的持续推进,它为这些有趣且必要的角色的创造提供了机会:

  1. AI 安全分析师 & AI 防御工程师: 鉴于 AI 潜在的恶意使用,专注于识别和缓解 AI 威胁的角色变得至关重要。

  2. AI 数据工程师 & 数据策展人: 这些专业人士将负责采购、组织和管理与大型语言模型相关的数据。

  3. AI 数据合规专家: AI 数据使用的法律复杂性需要专业人士能够处理 AI 法规中的复杂问题,特别是合规性,以确保减少法律风险。

  4. AI 政策监管员: 随着 AI 系统挑战现有法律框架,亟需熟练的专业人士与政府合作,制定有关 AI 实施的适当规则和标准。

  5. 首席 AI 伦理官和定量 AI 伦理学家: 将会有越来越大的需求来确保道德和公正的结果。公司可能会设立首席 AI 伦理官(以解决生成模型中的突出偏见)和定量 AI 伦理学家(通过量化这些模型中的偏见及其对利益相关者的影响来帮助实现这一目标)的角色。

  6. 提示工程师: 这一角色预计将会兴起,因为从业者通过制定强有力的提示来优化 AI 模型的输出,从而提高模型的效果和实用性。

  7. AI 体验工程师: 为了提升 AI 产品的用户友好性和实用性,公司将需要专门设计用户与 AI 互动的 AI 体验工程师。这将影响消费者在竞争市场中的产品偏好。

适应性和前瞻性将是拥抱这些新兴角色及其技能要求的关键,这些角色和要求正在被 AI 重塑的行业中不断发展。

你对这些工作职位中哪个最感兴趣?我是否遗漏了什么?留下评论吧!我很想知道!

如果你喜欢这篇文章,记得在 Medium 上关注我,获取更多类似的内容,或者通过电子邮件订阅我。你还可以将这篇文章分享给你网络中的数据科学爱好者!

如果你喜欢有关数据科学、机器学习和 AI 的有趣且富有信息的视频,请查看我的 YouTube 频道,我提供评论、教程和其他教育视频。

要获取我内容创作的每周更新和数据科学行业的额外学习资源,请订阅我的通讯,《数据滴滴!*

此外,考虑通过注册会员来支持我和其他成千上万的作者。

[## 使用我的推荐链接加入 Medium — Ken Jee

作为 Medium 会员,你的会员费的一部分将用于支持你阅读的作者,你可以全面访问每一个故事…

medium.com](https://medium.com/@kenneth.b.jee/membership?source=post_page-----b87191452f99--------------------------------)

使用 ChatGPT 创建完整产品的 7 个经验教训

原文:towardsdatascience.com/7-lessons-learned-on-creating-a-complete-product-using-chatgpt-462038856c85?source=collection_archive---------2-----------------------#2023-08-05

ChatGPT 的编码能力使得在短时间内完成整个产品变得非常简单——前提是你知道如何正确使用它

Shaked Zychlinski 🎗️Towards Data Science Shaked Zychlinski 🎗️

·

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

--

通过 StableDiffusion 生成

不久前,我与您分享了如何从 ChatGPT 创建我自己的法语导师(顺便说一下,它是开源的)。我描述了如何设计应用(尤其是它的后端)以及如何连接和配置不同的基于 AI 的服务。但有一件事我基本上跳过了,那就是如何创建应用的前端。你看,我不是前端程序员,我对 JavaScript 的了解仅限于知道需要将其放在 标签内。

但我心中设想的应用需要一个动态的用户界面。这意味着 HTML、JavaScript 和 CSS——但我完全不知道如何开始编写这些代码。

我知道的是我想要它看起来的样子。我心中有设计构想,我知道如果我知道如何编码的话,我会怎么做。因此,我决定采用一种新的和相当激进的方法——我会让 ChatGPT 为我编写代码。在那时,我已经有了向 ChatGPT 请求代码相关请求的经验,但从未尝试过如此复杂的任务。

好吧,当你读到这些文字时,你知道它成功了——我仅仅通过指示一个 LLM(大型语言模型)我想看到什么,就创建了一个完整的应用。我真的想再写一遍,只是为了确保我们都明白发生了什么:一个算法仅仅通过我用简单英语解释,就编写了整个应用。😲

尽管如此,这个过程并不像听起来那么简单——因此我想借此机会分享一些我在使用 ChatGPT 生成复杂代码时学到的技巧。

1. 亲自设计

LLMs 是创建代码和内容的强大工具,但它们不思考——它们只能执行请求(或者至少它们会尝试)。这意味着需要你来进行思考,特别是设计。在开始向生成模型发送请求之前,确保你知道最终产品应该是什么样子的。

关于这点——你需要自行研究什么技术栈最适合你。由于你需要将复杂的应用拆分为步骤(参见下方第 2 点),LLM 无法预见最终产品的样子,可能会使用次优的库或服务。

例如,ChatGPT 为我生成的第一个 UI 基于tkinter,它创建的是一个实际应用而不是网页 UI。这使得创建动态 UI 变得更加复杂(而且现在不太标准)。另一个尝试是基于steamlit,它使得创建非复杂 UI 非常简单,但同样没有设计用于复杂请求(例如:“仅在用户消息旁边添加播放录音按钮,但仅在用户录制了音频时”)。在我的情况下,最终决定使用Flask 是最优的选择。

2. 将其拆分为任务并从简单的开始

如果你要求 ChatGPT 一次性编写整个产品的代码,很有可能会得到一段有问题的代码。尽管它“聪明”,但不要期望它能够一次性关注所有细节。将你的设计拆分成任务和阶段,从比较简单的开始,然后逐步增加。

比如,这里是我最终设计的聊天界面,即我最初设计和计划的界面:

聊天机器人界面

你可以看到界面上有各种按钮和功能,而我最初对 ChatGPT 的提示是:

Write a Python web UI for a chatbot application. The text box where 
the user enters his prompt is located at the bottom of the screen, and 
all previous messages are kept on screen

没有特别的按钮,没有消息旁的个人头像,没有其他特别之处。只是一个简单的聊天界面,这将是我构建的核心。这个提示生成了 4 个文件:

  • 一个作为后端的 Python 文件(使用 Flask)

  • 一个 HTML 文件

  • 一个 JavaScript 文件(使用 jQuery)

  • 一个 CSS 文件

一旦我有了这些,我就可以开始使产品变得更复杂。

你可能会觉得我自相矛盾 —— 说要将应用拆分成小步骤,但又承认我的第一次提示生成了四个文件。每个对 ChatGPT 的请求之间,在完成任务所需的代码量与其非标准和特定性的权衡。要求生成整个聊天界面会得到比较通用的东西,但需要大量代码。要求“在导师消息旁边添加一个翻译按钮”,并且确保它位于消息气泡的右侧,始终在垂直中心,并且在 播放声音 按钮上方,这样的请求非常具体,因此它将是一个单独的请求。

3. 仔细解释你真正想要的

每个对你产品的请求和添加都可能涉及到多个文件的更改(每个文件可能有不止一个更改)。这意味着每次请求时都会创建新的变量、函数和端点,并且会从不同的位置引用它们。 ChatGPT 会为这些提供名称,并尽力提供有意义的名称 —— 但前提是你要解释清楚上下文。

比如,如果你想在你的产品中添加一个“保存”按钮,最好像这样提问:

Add a "Save Session" button to the left of the text box. It should have 
a floppy-disk icon. Once clicked, all messages on the UI will be saved to 
a JSON file named "saved_session.json"

而不是这样的缺乏上下文的提示:

Add a button to the left of the text box wth a floppy-disk icon. Once 
clicked, all messages on the UI will be saved to a JSON file.

倾向于丰富上下文的提示会产生更好的命名约定。

4. 非常清楚地知道你要问什么

这是我遇到的一个真正的问题,之前没预见到:我希望 UI 显示来自我的法语导师的生成文本,并且是实时流式显示,类似于 ChatGPT 的效果。我使用的 Python API(OpenAI ChatCompletion API)返回了一个 Python Generator,这需要被消费并打印到屏幕上。因此,我问 ChatGPT:

Write a JavaScript function that consumes the generator and updates the 
message text one item at a time

我不知道的是——因为我从未写过任何严肃的 JavaScript 代码——我要求的东西是不可能的;JavaScript 无法处理 Python 生成器。结果是 ChatGPT 给了我各种奇怪且完全无用的解决方案,因为它试图完全按照我的要求——修改 JavaScript 代码。

你必须记住,ChatGPT 尝试完全按照你的要求来满足你的请求,只要这些请求不违反其指导方针。在那时我真正需要的是它告诉我我要求的东西很愚蠢,但这并不是它的工作方式。

这个问题只有在我发现自己在要求不可能的东西(老办法——Google 和 StackOverflow)之后才得到解决,并将我的提示更改为类似以下内容:

Given the response generator, add functionality to consume the generator 
and updates the message text one item at a time 

这导致对 JavaScript Python 文件进行了修改,从而实现了预期的结果。

使用 StableDiffusion 生成

5. LLM 无法还原其代码(以及如何还原)

虽然 ChatGPT 在编写代码方面表现出色,但它仍然只是一个语言模型,它在还原自己修改的内容方面做得不好——尤其是当你要求它还原并回到两三次之前的提示时。与 LLMs 进行分阶段生成代码时,我强烈建议始终保留你满意的最后一个工作版本的代码副本;这样,如果 ChatGPT 添加的新代码出现问题且无法修复,你可以轻松将代码还原到最后一个正常工作的版本。

但有一个问题——因为如果你恢复了你的代码,你还需要恢复 ChatGPT,以确保它确切知道你代码现在的样子。最好的方法是启动一个新会话,并用类似下面的提示开始:

I'm building a chatbot application. Here is my code so far:

HTML:

你的 HTML 代码


JavaScript:

你的 JavaScript 代码


CSS:

你的 CSS 代码


Python:

你的 Python 代码


Add a "Save Session" button to the left of the text box. It should have 
a floppy-disk icon. Once clicked, all messages on the UI will be saved to 
a JSON file named "saved_session.json"

(你也可以将文件上传到 ChatGPT 的代码解释器,那时还不可用)。如果提示过长无法作为单条消息发送,将其拆分为两部分。在这两条消息之间,点击“停止生成”,以防止机器人插入不必要的文本。

6. 不要太长时间地与之对抗

使用 ChatGPT 编码的一个酷炫之处是,如果它写出的代码有问题,或者代码没有按预期执行,你可以直接发送错误信息,它会相应地修复代码。

但这并不总是发生。有时 ChatGPT 无法修复错误,或者反而引入了另一个错误。我们会将新的错误信息发送给它,并再次要求它修复。如果这种情况发生超过两三次,代码有可能会变得非常破碎或过度修改,从而完全无法工作。如果你达到了这一点,请停止,恢复(见上文),并重新表述你的请求。

7. 学习如何编写提示

尽管 ChatGPT 的核心在于你可以使用日常语言与其互动,但正确编写提示可以对结果产生巨大影响。我真的推荐花时间学习如何做到这一点。例如,这个由 OpenAI 和 DeepLearning.AI 提供的免费课程是必修的,特别是关于如何将指令、代码和示例结合在一个提示中的课程。

你可以学到的最重要的事情之一是确保在提示中自由文本和代码之间有明显的区别。因此,避免这样做:

Here's a Python function: 
def func(x): 
  return x*2
Change it so it'll return the root of the absolute value of the input if 
it's negative.

这样写:

Here's a Python function: 

def func(x):

return x*2

Change it so it'll return the root of the absolute value of the input if 
it's negative.

此外,如果可能的话,提供输入输出示例。这是向 LLM 解释它应该做什么的最佳方法,因为它消除了你请求中的任何模糊性(如果输入是正数,模型应该返回什么?保持 x2 还是什么都不做?*):

Here's a Python function: 

def func(x):

return x*2

Change it so it'll return the root of the absolute value of the input if 
it's negative.

Examples:
Input: 2, Output: 4
Input: -9, Output: 3

额外提示:选择合适的 LLM

记住,“ChatGPT”是一个网络产品的名称,而不是模型本身。免费版让你使用 GPT-3.5,而付费版则包括 GPT-4,它在编码任务中表现显著更好。新的代码解释器也使其表现更好,因为它可以运行和测试其代码。

即使你决定选择另一个 LLM 来合作,确保你选择的那个在编码任务中表现良好。否则,这些提示将毫无帮助。

在总结这一切时,我想最重要的是认识到与 LLMs 沟通时每个词都很重要。LLMs 不会思考,也不能真正理解我们想要什么,除非明确以它们需要的方式向它们解释,因为——谢天谢地——它们还不是人类(还没?),它们只是工具。就像每一个工具一样——如果你不知道怎么使用它,你就无法完成任何工作。我确实希望你能在下一个项目中找到这些提示的用处!

使用 StableDiffusion 生成

7 种最常用的特征工程技术

原文:towardsdatascience.com/7-of-the-most-used-feature-engineering-techniques-bcc50f48474d

使用 Scikit-Learn、Tensorflow、Pandas 和 Scipy 进行实用特征工程

多米尼克·波尔策Towards Data Science 多米尼克·波尔策

·发布在Towards Data Science ·37 分钟阅读·2023 年 1 月 9 日

--

7 种最常用的特征工程技术——图像由作者提供

目录

Introduction 1\. Encoding
  1.1 Label Encoding using Scikit-learn
  1.2 One-Hot Encoding using Scikit-learn, Pandas and Tensorflow
2\. Feature Hashing
  2.1 Feature Hashing using Scikit-learn
3\. Binning / Bucketizing
  3.1 Bucketizing using Pandas
  3.2 Bucketizing using Tensorflow
  3.3 Bucketizing using Scikit-learn
4\. Transformer
  4.1 Log-Transformer using Numpy
  4.2 Box-Cox Function using Scipy
5\. Normalize / Standardize
  5.1 Normalize and Standardize using Scikit-learn
6\. Feature Crossing
  6.1 Feature Crossing in Polynomial Regression
  6.2 Feature Crossing and the Kernel-Trick
7\. Principal Component Analysis (PCA)
  7.1 PCA using Scikit-learn
Summary
References

介绍

特征工程描述了制定相关特征的过程,这些特征尽可能准确地描述了基础的数据科学问题,并使算法能够理解和学习模式。换句话说:

你提供的特征作为一种方式,将你对世界的理解和知识传达给你的模型

每个特征描述了一种信息“片段”。这些片段的总和使算法能够对目标变量得出结论——至少当你的数据集实际包含有关目标变量的信息时。

根据福布斯杂志,数据科学家将约 80%的时间花在收集和准备相关数据上,其中数据清理和数据组织单独占据了约 60%的时间。

但这段时间是花得值得的。

我相信数据的质量以及数据集特征的恰当准备,对机器学习模型的成功的影响大于机器学习管道中的任何其他部分:

标准机器学习管道——受[Sarkar et al., 2018]的启发

福布斯杂志认为的“清理和组织”通常在机器学习管道中分解为两个到三个子类别(我在上面的图像中用黄色背景突出显示了它们):

(1) 数据(预处理): 数据的初步准备——例如,平滑信号、处理异常值等。

(2) 特征工程: 定义模型的输入特征——例如,通过使用快速傅里叶变换(FFT)将(声学)信号转换为频域,允许我们从原始信号中提取关键的信息。

(3) 特征选择: 选择对目标变量有显著影响的特征。通过选择重要特征并减少维度,我们可以显著降低建模成本,提高模型的鲁棒性和性能。

我们为什么需要特征工程?

Andrew Ng 经常提倡所谓的 数据中心方法,强调选择和策划数据的重要性,而不仅仅是试图收集越来越多的数据。目标是确保数据的高质量和与所解决问题的相关性,并通过数据清洗、特征工程和数据增强不断改进数据集。

为什么我们在拥有深度学习的情况下还需要特征工程和数据中心方法?

这种方法特别适用于收集大量数据成本高昂或其他困难的使用场景。[Brown, 2022] 例如,当数据难以访问,或存在严格的法规或其他障碍来收集和存储大量数据时:

  • 在制造业中,为生产设施配备全面的传感器技术并将其连接到数据库是昂贵的。因此,许多工厂尚未收集数据。即使机器能够收集和存储数据,它们也很少与中央数据湖连接。

  • 在供应链背景下,每家公司每天处理的订单数量有限。因此,如果我们想预测需求非常不规律的产品的需求,有时我们只有少量的数据点可用。

在这些领域,特征工程可能对提高模型性能具有最大影响。这里需要工程师和数据科学家的创造力来将数据集的质量提升到足够的水平。这个过程很少是直接的,而是实验性的和迭代的。

当人类分析数据时,他们通常会利用过去的知识和经验来帮助理解模式并做出预测。例如,如果有人尝试估算不同国家的温度,他们可能会考虑国家相对于赤道的位置,因为他们知道温度往往在靠近赤道的地方较高。

然而,机器学习模型并不像人类一样具有内在的概念和关系理解能力。它只能从提供给它的数据中学习。因此,任何人类用于解决问题的背景信息或上下文必须以数值形式显式地包含在数据集中。

平均年温度按国家划分 [Wikimedia]

那么我们需要什么数据才能使模型比人类更聪明?

我们可以使用 Google Maps API 查找每个国家的地理坐标(经度和纬度)。此外,我们还可以收集每个国家的海拔信息和距离最近水体的距离。通过收集这些额外数据,我们希望识别并考虑可能影响每个国家温度的因素。

假设我们收集了一些可能影响温度的数据,接下来该怎么办?

一旦我们拥有足够描述问题特征的数据,我们仍然需要确保计算机能够理解这些数据。类别数据、日期等必须转换为数值。

在这篇文章中,我将描述一些常用的原始数据准备技术。某些技术用于将类别数据转换为机器学习模型可以理解的数值,如编码向量化。其他技术用于处理数据分布,如变换器分箱,这些技术可以帮助以某种方式归一化或标准化数据。还有其他技术用于通过生成新特征来减少数据集的维度,如哈希主成分分析 (PCA)

自己试试吧……

如果您想跟随本文并尝试这些方法,可以使用下面的代码库,其中包含 Jupyter Notebook 中的代码片段和使用的数据集:

[## GitHub - polzerdo55862/7-feature-engineering-techniques

目前无法执行该操作。您在其他标签页或窗口中登录了。您已在另一个标签页或窗口中退出登录……

github.com](https://github.com/polzerdo55862/7-feature-engineering-techniques?source=post_page-----bcc50f48474d--------------------------------)

1. 编码

特征编码是将类别数据转换为 ML 算法可以理解的数值的过程。有几种类型的编码,包括标签编码和独热编码。

标签编码和热编码 — 作者提供的图片

标签编码涉及为每个类别值分配一个数字值。如果类别值之间存在固有顺序,例如从 A 到 F 的等级,可以将其编码为从 1 到 5(或 6)的数字值。然而,如果类别值之间没有固有的顺序,则标签编码可能不是最佳方法。

另外,你也可以使用一次性编码将类别值转换为数值值。在一次性编码中,类别值的列被拆分成几列,每列对应一个独特的类别值。

例如,如果类别值是从 A 到 F 的等级:

  • 我们将新增五列,每列对应一个等级。

  • 数据集中每一行在与其等级对应的列中将有一个值为 1,在所有其他列中则为 0。

  • 这会导致所谓的稀疏矩阵,其中大多数值为 0。

一次性编码的缺点是它可能显著增加数据集的大小,如果要编码的列包含数百或数千个独特的类别值,这可能会成为问题。

接下来,我将使用 Pandas、Scikit-learn 或 Tensorflow 对数据集应用标签和一次性编码。以下示例中,我们使用的是人口普查收入数据集:

数据集:人口普查收入 [许可证: CC0: 公共领域]

archive.ics.uci.edu/ml/datasets/census+income

www.kaggle.com/datasets/uciml/adult-census-income

人口普查收入数据集描述了美国个人的收入情况。它包括他们的年龄、性别、婚姻状况和其他人口统计信息,以及他们的年收入,分为两类:超过 50,000 美元或低于 50,000 美元。

对于标记示例,我们使用的是人口普查数据集中描述个人最高学历的“教育”列。它包含的信息例如个人是否完成了高中、大学、获得了研究生学位或其他形式的教育。

首先,让我们加载数据集:

import pandas as pd

# load dataset - census income
census_income = pd.read_csv(r'../input/income/train.csv')

1.1 使用 Scikit-learn 进行标签编码

标签编码是将类别值转换为数值值的最简单方法。这是一个简单的过程,将每个类别分配一个数值。

标签编码——图片来源于作者

你可以在 Pandas、Scikit-Learn 和 Tensorflow 中找到合适的库。我使用的是 Scikit-Learn 的标签编码函数。它随机将整数分配给独特的类别值,这是最简单的编码方式:

from sklearn import preprocessing

# define and fit LabelEncoder
le = preprocessing.LabelEncoder()
le.fit(census_income["education"])

# Use the trained LabelEncoder to label the education column
census_income["education_labeled"] = le.transform(census_income["education"])

display(census_income[["education", "education_labeled"]])

这种编码方式可能会给一些算法带来问题,因为分配的整数不一定反映类别之间的任何固有顺序或关系。例如,在上述情况中,算法可能会假设类别博士(10)和高中毕业(11)彼此更为相似,而不是博士(10)和学士(9),并且高中毕业(11)比博士(10)“高”。

标签编码结果解释 — 图像由作者提供

如果我们对数据集的特定领域或主题有一些了解,我们可以利用它确保标签编码过程反映任何固有的顺序。以我们的例子为例,我们可以尝试考虑获得不同学位的顺序来标记教育水平,例如:

博士学位是高于硕士和本科的更高学术学位。硕士学位高于本科。

要将这种手动映射应用于数据集,我们可以使用pandas.map函数:

education_labels = {'Doctorate':5, 'Masters':4, 'Bachelors':3, 'HS-grad':2, '12th':1, '11th':0}

census_income['education_labeled_pandas']=census_income['education'].map(education_labels)

census_income[["education", "education_labeled_pandas"]]

但这如何影响模型构建过程?

让我们用标记编码数据构建一个简单的线性回归模型:

对于下面的图,目标变量定义为个人收入超过 $50,000 的概率

  • 在左图中,分类值(如“本科”和“博士”)被随机分配给数值。

  • 在右图中,分配给分类值的数值反映了通常获得学位的顺序,较高的教育学位分配较高的数值。

右图显示了教育水平与收入之间的明显相关性,这可以通过一个简单的线性模型来表示。

与之相反,左图展示了教育属性与目标变量之间的关系,这需要一个更复杂的模型才能准确表示。这可能会影响结果的可解释性,增加过拟合的风险,并增加拟合模型所需的计算工作量。

标签编码:训练有序和无序“教育”特征的线性回归模型 — 图像由作者提供

当值没有固有的顺序或者我们没有足够的信息进行映射时,我们该怎么办?

如果分类值完全没有固有的顺序,使用将类别转换为一组数值变量而不引入任何偏见的编码方法可能更好。独热编码就是一种合适的方法。

1.2 使用 Scikit-learn、Pandas 和 Tensorflow 进行独热编码

独热编码是一种将分类数据转换为数值数据的技术。它通过为数据集中的每个唯一类别创建一个新的二进制列,并将属于该类别的行赋值为 1,将不属于该类别的行赋值为 0 来实现这一点。

→ 这个过程通过不假设类别之间存在任何固有顺序,帮助避免对数据引入偏见。

One-Hot Encoding — 图像由作者提供

One-Hot 编码的过程相当简单。我们可以自己实现它,也可以使用现有的函数之一。Scikit-learn 有.preprocessing.OneHotEncoder()函数,Tensorflow 有.one_hot()函数,Pandas 有.get_dummies()函数。

  • Pandas.get_dummies()
import pandas as pd

education_one_hot_pandas = pd.get_dummies(census_income["education"], prefix='education')
education_one_hot_pandas.head(2)
  • Sklearn.preprocessing.LabelBinarizer()
from sklearn import preprocessing

lb = preprocessing.LabelBinarizer()
lb.fit(census_income["education"])

education_one_hot_sklearn_binar = pd.DataFrame(lb.transform(census_income["education"]), columns=lb.classes_)
education_one_hot_sklearn_binar.head(2)
  • Sklearn.preprocessing.OneHotEncoder()
from sklearn.preprocessing import OneHotEncoder

# define and fit the OneHotEncoder
ohe = OneHotEncoder()
ohe.fit(census_income[['education']])

# transform the data
education_one_hot_sklearn = pd.DataFrame(ohe.transform(census_income[["education"]]).toarray(), columns=ohe.categories_[0])
education_one_hot_sklearn.head(3)

One-hot 编码的问题是它可能导致具有高维度的大型稀疏数据集。

使用 one-hot 编码将具有 10,000 个唯一值的分类特征转换为数值数据,会导致在数据集中创建 10,000 列,每列表示一个不同的类别。这在处理大型数据集时可能会成为问题,因为它会迅速消耗大量内存和计算资源。

如果内存和计算能力有限,可能 需要减少数据集中的特征数量 以避免遇到内存或性能问题。

我们如何减少维度以节省内存?

在执行此操作时,重要的是尽可能减少信息的丢失。这可以通过仔细选择保留或删除哪些特征、使用如特征选择或维度减少等技术来识别和删除冗余或不相关的特征来实现。[Sklearn.org][Wikipedia, 2022]

在本文中,我将描述两种可能的方式来减少数据集的维度:

  1. 特征哈希 — 见第二部分。特征哈希

  2. 主成分分析(PCA) — 见第七部分。PCA

信息丢失 vs. 速度 vs. 内存

可能没有一种“完美”的方法来减少数据集的维度。一种方法可能更快,但可能导致大量信息丢失,而另一种方法则保留更多信息,但需要大量计算资源(这也可能导致内存问题)。

2. 特征哈希

特征哈希主要是一种维度减少技术,通常用于自然语言处理。然而,当我们需要对具有几百或几千个唯一类别的分类特征进行向量化时,哈希也可以很有用。通过哈希,我们可以通过将多个唯一值分配到相同的哈希值来限制维度的增加。

→ 因此,哈希是一种低内存的替代方法,用于 OneHot 编码和其他特征向量化方法。

哈希通过将哈希函数应用于特征并直接使用哈希值作为索引,而不是构建哈希表并单独查找索引,来进行工作。Sklearn 中的实现基于 Weinberger [Weinberger et al., 2009]。

我们可以将其分解为 2 个步骤:

  • 第 1 步: 首先使用哈希函数将分类值转换为哈希值。Scikit-learn 中的实现使用了 32 位变体的 MurmurHash3。

哈希技巧 — 作者提供的图像

import sklearn 
import pandas as pd

# load data set
census_income = pd.read_csv(r'../input/income/train.csv')
education_feature = census_income.groupby(by=["education"]).count().reset_index()["education"].to_frame()

############################################################################################################
# Apply the hash function, here MurmurHash3 
############################################################################################################
def hash_function(row):
    return(sklearn.utils.murmurhash3_32(row.education))

education_feature["education_hash"] = education_feature.apply(hash_function, axis=1)
education_feature

  • 第 2 步: 接下来,我们通过对特征值应用mod 函数来降低维度。使用 mod 函数,我们计算哈希值除以n_features(输出向量的特征数量)后的余数。

建议使用 2 的幂作为 n_features 参数;否则,特征将无法均匀映射到列中。

示例:人口普查收入数据集 → 对教育列进行编码

人口普查收入数据集的“教育”列包含 16 个唯一值:

  • 使用独热编码,将向数据集中添加 16 列,每列代表 16 个唯一值中的一个。

→ 为了降低维度,我们将n_features设置为 8。

这不可避免地导致“冲突”,即不同的类别值被映射到相同的哈希列。换句话说,我们将不同的值分配到同一个桶中。这类似于我们在分箱/桶化(参见第三部分 分箱/桶化)中所做的。在特征哈希中,我们通过将分配到同一桶的值串联起来并存储在一个列表中(称为“分离链”)来处理冲突。

特征哈希:通过计算余数作为新列索引来降低维度 — 作者提供的图像

############################################################################################################
# Apply mod function
############################################################################################################
n_features = 8

def mod_function(row):
    return(abs(row.education_hash) % n_features)

education_feature["education_hash_mod"] = education_feature.apply(mod_function, axis=1)
education_feature.head(5)

为我们执行上述步骤的函数是来自Scikit-learnHashingVectorizer函数。

2.1 使用 Scikit-learn 进行特征哈希

sklearn.feature_extraction.text.HashingVectorizer

from sklearn.feature_extraction.text import HashingVectorizer

# define Feature Hashing Vectorizer
vectorizer = HashingVectorizer(n_features=8, norm=None, alternate_sign=False, ngram_range=(1,1), binary=True)

# fit the hashing vectorizer and transform the education column
X = vectorizer.fit_transform(education_feature["education"])

# transformed and raw column to data frame
df = pd.DataFrame(X.toarray()).assign(education = education_feature["education"])
display(df)

3. 分箱 / 桶化

分箱用于分类数据和数值数据。顾名思义,目标是将特征的值映射到“分箱”中,并用表示该分箱的值替换原始值。

例如,如果我们有一个值范围从 0 到 100 的数据集,并且我们想将这些值分组为大小为 10 的分箱,我们可以为值 0-9、10-19、20-29 等创建分箱。

→ 在这种情况下,原始值将被替换为表示它们所属的分箱的值,例如 10、20、30 等。这有助于可视化和分析数据。

由于我们减少了数据集中唯一值的数量,这有助于:

  • 防止过拟合

  • 提高模型的鲁棒性并减轻异常值的影响

  • 降低模型复杂性和训练模型所需的资源

系统化的分箱可以帮助算法更容易高效地检测潜在模式。如果我们在定义分箱之前能形成一个假设,这会特别有帮助。

分箱可用于数值和分类值,例如:

对类别和数值特征进行分桶 — 图片由作者提供

在以下内容中,我描述了如何使用三个示例来处理数值型和类别型属性:

  1. 数值型 — 在构建流媒体推荐系统时如何使用分桶——这是我在[Zheng, Alice, and Amanda Casari. 2018]的书Feature Engineering for Machine Learning中找到的用例

  2. 数值型 — 人口普查收入数据集:对“年龄”列应用分桶

  3. 类别型 — 供应链中的分桶:根据目标变量将国家分配到桶中

示例 1:流媒体推荐系统——一首歌或视频有多受欢迎?

如果你想开发一个推荐系统,将歌曲的相对受欢迎程度分配数值是很重要的。最显著的属性之一是点击次数,即用户听这首歌的频率。

然而,一个用户听一首歌 1000 次并不一定比听 50 次的用户喜欢它多 20 倍。分桶有助于防止过拟合。因此,将歌曲的点击次数分为类别可能是有益的。: [Zheng, Alice and Amanda Casari. 2018]

点击次数 >=10: 非常受欢迎

点击次数 >=2: 流行

点击次数 <2: 中性,无可能的声明

示例 2:年龄与收入的关系

对于第二个示例,我再次使用人口普查收入数据集。目标是将个体分组到年龄桶中。

假设 — 这个观点是,个人的具体年龄可能对他们的收入影响不大,而是他们所处的生活阶段。例如,一个 20 岁还在上学的人可能与一个 30 岁的年轻专业人士的收入不同。类似地,一个 60 岁仍在全职工作的人可能与一个 70 岁退休的人收入不同,而如果一个人是 40 岁或 50 岁,收入差异可能不大。

3.1 使用 Pandas 进行分桶

为了按年龄对数据进行分桶,我定义了三个“桶”:

  • 年轻 — 28 岁及以下

  • 中年 — 29 至 59 岁

  • 老年 — 60 岁及以上

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

# creating a dictionary
sns.set_style("whitegrid")
plt.rc('font', size=16) #controls default text size
plt.rc('axes', titlesize=16) #fontsize of the title
plt.rc('axes', labelsize=16) #fontsize of the x and y labels
plt.rc('xtick', labelsize=16) #fontsize of the x tick labels
plt.rc('ytick', labelsize=16) #fontsize of the y tick labels
plt.rc('legend', fontsize=16) #fontsize of the legend

# load dataset - census income
census_income = pd.read_csv(r'../input/income/train.csv')

# define figure
fig, (ax1, ax2) = plt.subplots(2)
fig.set_size_inches(18.5, 10.5)

# plot age histogram
age_count = census_income.groupby(by=["age"])["age"].count()
ax1.bar(age_count.index, age_count, color='black')
ax1.set_ylabel("Counts")
ax1.set_xlabel("Age")

# binning age
def age_bins(age):
    if age < 29:
        return "1 - young"
    if age < 60 and age >= 29:
        return "2 - middle-aged"
    else:
        return "3 - old-aged"

# apply trans. function
census_income["age_bins"] = census_income["age"].apply(age_bins)

# group and count all entries in the same bin
age_bins_df = census_income.groupby(by=["age_bins"])["age_bins"].count()

ax2.bar(age_bins_df.index, age_bins_df, color='grey')
ax2.set_ylabel("Counts")
ax2.set_xlabel("Age")

3.2 使用 Tensorflow 进行分桶

Tensorflow 提供了一个名为feature columns的模块,包含了一系列用于帮助处理原始数据的函数。特征列是组织和解释原始数据的函数,以便机器学习算法可以解读和利用这些数据进行学习。[Google Developers, 2017]

TensorFlow 的 feature_column.bucketized_column API 提供了一种对数值数据进行分桶的方法。这个 API 接受一个数值列,并根据指定的边界创建一个桶化列。输入的数值列可以是连续的或离散的。

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import feature_column
from tensorflow.keras import layers

# load dataset - census income
census_income = pd.read_csv(r'../input/income/train.csv')
data = census_income.to_dict('list')

# A utility method to show the transformation from feature column
def demo(feature_column):
    feature_layer = layers.DenseFeatures(feature_column)
    return feature_layer(data).numpy()

age = feature_column.numeric_column("age")
age_buckets = feature_column.bucketized_column(age, boundaries=[30,50])
buckets = demo(age_buckets)

# add buckets to the data set
buckets_tensorflow = pd.DataFrame(buckets)

# define boundaries for buckets
boundary1 = 30
boundary2 = 50

# define column names for buckets
bucket1=f"age<{boundary1}"
bucket2=f"{boundary1}<age<{boundary2}"
bucket3=f"age>{boundary2}"

buckets_tensorflow_renamed = buckets_tensorflow.rename(columns = {0:bucket1,
                                                                    1:bucket2,
                                                                    2:bucket3})

buckets_tensorflow_renamed.assign(age=census_income["age"]).head(7)

我们可以通过使用KBinsDiscretizer来自Scikit-learn.做到这一点。

3.3 使用 Scikit-learn 进行分桶化

from sklearn.preprocessing import KBinsDiscretizer

# define bucketizer
est = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform')
est.fit(census_income[["age"]])

# transform data
buckets = est.transform(census_income[["age"]])

# add buckets column to data frame
census_income_bucketized = census_income.assign(buckets=buckets)[["age", "buckets"]]
census_income_bucketized

最后一个关于分类值的示例 …

没有一种普遍适用的最佳方式来以数字形式表示分类变量。使用哪种方法,取决于具体的使用案例和你希望传达给模型的信息。不同的编码方法可以突出数据的不同方面,因此选择最适合你特定使用案例的方法非常重要。

示例 3: 分类值分桶化 — 例如 国家

供应链 — 在评估供应商/分包商的可靠性时,地区、气候和运输类型等因素可以影响交货时间的准确性。在我们的资源规划系统中,我们通常存储供应商的国家和城市。由于一些国家在某些方面相似而在其他方面完全不同,突出与使用案例相关的方面可能是有意义的。

  • 如果我们想突出国家之间的距离并将同一地区的国家分组,我们可以为大陆定义区间

  • 如果我们更关心价格或可能的收入,人均 GDP 可能更为重要 → 在这里我们可以将“高成本”国家如美国、德国和日本归入一个区间

  • 如果我们想突出一般的天气条件,我们可以将北方国家如加拿大和挪威放在同一个区间

示例: 使用不同属性对国家进行分桶化 — 图片由作者提供

4. Transformer

转换技术是用来改变数据集形式或分布的方法。一些常见的转换技术,如对数Box-Cox 函数,用于将不符合正态分布的数据转换为更对称、更接近钟形曲线的数据。这些技术在数据需要满足特定统计模型或技术要求的假设时可能会很有用。使用的具体转换方法可能取决于期望的结果和数据的特性。

Transformers: 转换特征值分布 — 图片由作者提供

4.1 使用 Numpy 的 Log-Transformer

对数转换器用于通过对每个值应用对数函数来改变数据的尺度。这种转换通常用于将高度偏斜的数据转换为更接近正态分布的数据。

数据偏斜绝不是不寻常的现象,在许多情况下,数据自然或人为地呈现偏斜,例如:

  • 词汇在语言中的使用频率遵循一种被称为齐普夫定律的模式

  • 人类感知不同刺激的方式遵循一种由史蒂文斯幂函数描述的模式

数据的分布不对称,可能存在大量比大多数数据值更大或更小的长尾值。

正常分布与幂律分布 — 图片由作者提供(灵感来自 [Geeksforgeeks, 2022]

某些类型的算法可能难以处理这种类型的数据,可能会产生较少的准确或可靠结果。应用幂变换,如 Box-Cox 变换或对数变换,可以帮助调整数据的尺度,使其更适合这些算法处理。[Geeksforgeeks, 2020]

让我们看看变换器如何影响实际数据。因此,我使用了世界人口、在线新闻流行度和波士顿住房数据集:

数据集 1: 世界人口 [许可证: CC BY 3.0 IGO]

population.un.org/wpp/Download/Standard/CSV/

数据集包含了全球每个国家的人口数据。[联合国, 2022]

数据集 2: 在线新闻流行度 [许可证: CC0: 公共领域]

archive.ics.uci.edu/ml/datasets/online+news+popularity

该数据集总结了 Mashable 在两年内发布的文章的异质特征。目标是预测社交网络中的分享数量(流行度)。

数据集 3: 波士顿住房数据集 [许可证: CC0: 公共领域]

www.cs.toronto.edu/~delve/data/boston/bostonDetail.html

波士顿住房数据集由美国人口普查局在 1970 年代末和 1980 年代初收集。它包含波士顿(马萨诸塞州)地区房屋的中位数价值的信息。该数据集包括 13 个属性,例如每栋房屋的平均房间数、到就业中心的平均距离和财产税率,以及该地区房屋的中位数价值。

以下函数帮助绘制数据集变换前后的分布图:

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from scipy import stats
import seaborn as sns

def plot_transformer(chosen_dataset, chosen_transformation, chosen_feature = None, box_cox_lambda = 0):
    plt.rcParams['font.size'] = '16'
    sns.set_style("darkgrid", {"axes.facecolor": ".9"})

    ##################################################################################################
    # choose dataset
    ##################################################################################################
    if chosen_dataset == "Online News Popularity":
        df = pd.read_csv("../input/uci-online-news-popularity-data-set/OnlineNewsPopularity.csv")
        X_feature = " n_tokens_content"
    elif chosen_dataset == "World Population":
        df = pd.read_csv("../input/world-population-dataset/WPP2022_TotalPopulationBySex.csv")
        df = df[df["Time"]==2020]
        df["Area"] = df["PopTotal"] / df["PopDensity"]
        X_feature = "Area"
    elif chosen_dataset == "Housing Data":
        df = pd.read_csv("../input/housing-data-set/HousingData.csv")
        X_feature = "AGE"

    # in case you want to plot the histogram for another feature
    if chosen_feature != None:
        X_feature = chosen_feature

    ##################################################################################################
    # choose type of transformation
    ##################################################################################################
    #chosen_transformation = "box-cox" #"log", "box-cox"

    if chosen_transformation == "log":
        def transform_feature(df, X_feature):
            # We add 1 to number_of_words to make sure we don't have a null value in the column to be transformed (0-> -inf) 
            return (np.log10(1+ df[[X_feature]]))

    elif chosen_transformation == "box-cox":
        def transform_feature(df, X_feature):
            return stats.boxcox(df[[X_feature]]+1, lmbda=box_cox_lambda)
            #return stats.boxcox(df[X_feature]+1)

    ##################################################################################################
    # plot histogram to chosen dataset and X_feature
    ##################################################################################################
    # figure settings
    fig, (ax1, ax2) = plt.subplots(2)
    fig.set_size_inches(18.5, 10.5)

    ax1.set_title(chosen_dataset)
    ax1.hist(df[[X_feature]], 100, facecolor='black', ec="white")
    ax1.set_ylabel(f"Count {X_feature}")
    ax1.set_xlabel(f"{X_feature}")

    ax2.hist(transform_feature(df, X_feature), 100, facecolor='black', ec="white")
    ax2.set_ylabel(f"Count {X_feature}")
    ax2.set_xlabel(f"{chosen_transformation} transformed {X_feature}")

    fig.show()

在接下来的内容中,我不仅测试数据变换后的效果,还对这种变换如何影响模型构建过程感兴趣。我使用多项式回归来建立简单的回归模型,并比较模型的性能。模型的输入数据仅为 2 维:

  • 对于Only News Popularity 数据集,我们尝试建立一个模型,该模型根据在线文章的“n_tokens”预测“分享数”——也就是说,我们试图建立一个模型,根据文章的标记数量/长度预测文章的受欢迎程度。

  • 对于World Population data set,我们建立了一个简单的模型,该模型仅根据一个输入特征——国家的面积——来预测人口。

示例 1: 世界人口数据集 — 国家面积

plot_transformer(chosen_dataset = "World Population", chosen_transformation = "log")

在图表中,你可以看到原始分布,虽然偏向左侧,但已经被转化为一个更对称的分布。

我们也试试在线新闻受欢迎度数据集:

示例 2: 在线新闻受欢迎度数据集 — 根据文章长度的文章受欢迎程度

plot_transformer(chosen_dataset = "Online News Popularity", chosen_transformation = "log")

这个例子更好地展示了效果。偏斜的数据被转化为几乎“完美”的正态分布数据。

但是这会对模型构建过程产生积极影响吗?为此,我为原始数据和转化数据创建了多项式模型,并比较它们的表现:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
import plotly.graph_objects as go
import pandas as pd
from scipy import stats
import plotly.express as px
import seaborn as sns

def plot_cross_val_score_comparison(chosen_dataset):
    scores_raw = []
    scores_transformed = []
    degrees = []

    if chosen_dataset == "Online News Popularity":
        df = pd.read_csv("../input/uci-online-news-popularity-data-set/OnlineNewsPopularity.csv")
        X = df[[" n_tokens_content"]]
        y = df[" shares"]
    elif chosen_dataset == "World Population":
        df = pd.read_csv("../input/world-population-dataset/WPP2022_TotalPopulationBySex.csv")
        df = df[df["Time"]==2020]
        df["Area"] = df["PopTotal"] / df["PopDensity"]
        X = df[["Area"]]
        y = df["PopTotal"]

    for i in range(1,10):
        degree = i
        polynomial_features = PolynomialFeatures(degree=degree, include_bias=False)

        linear_regression = LinearRegression()
        pipeline = Pipeline(
            [
                ("polynomial_features", polynomial_features),
                ("linear_regression", linear_regression),
            ]
        )

        # Evaluate the models using crossvalidation
        scores = cross_val_score(
            pipeline, X, y, scoring="neg_mean_absolute_error", cv=5
        )

        scores_raw.append(scores.mean())
        degrees.append(i)

        #########################################################################
        # Fit model with transformed data
        #########################################################################
        def transform_feature(X):
            # We add 1 to number_of_words to make sure we don't have a null value in the column to be transformed (0-> -inf) 
            #return (np.log10(1+ X))
            return stats.boxcox(X+1, lmbda=0)

        X_trans = transform_feature(X)

        pipeline.fit(X_trans, y)

        # Evaluate the models using crossvalidation
        scores = cross_val_score(
            pipeline, X_trans, y, scoring="neg_mean_absolute_error", cv=5
        )

        scores_transformed.append(scores.mean())

    plot_df = pd.DataFrame(degrees, columns=["degrees"])
    plot_df = plot_df.assign(scores_raw = np.abs(scores_raw))
    plot_df = plot_df.assign(scores_transformed = np.abs(scores_transformed))

    fig = go.Figure()
    fig.add_scatter(x=plot_df["degrees"], y=plot_df["scores_transformed"], name="Scores Transformed", line=dict(color="#0000ff"))

    # Only thing I figured is - I could do this 
    #fig.add_scatter(x=plot_df['degrees'], y=plot_df['scores_transformed'], title='Scores Raw')  
    fig.add_scatter(x=plot_df['degrees'], y=plot_df['scores_raw'], name='Scores Raw')

    # write scores for raw and transformed data in one data frame and find degree that shows the mininmal error score
    scores_df = pd.DataFrame(np.array([degrees,scores_raw, scores_transformed]).transpose(), columns=["degrees", "scores_raw", "scores_transformed"]).abs()
    scores_df_merged = scores_df[["degrees","scores_raw"]].rename(columns={"scores_raw":"scores"}).append(scores_df[["degrees", "scores_transformed"]].rename(columns={"scores_transformed":"scores"}))
    degree_best_performance = scores_df_merged[scores_df_merged["scores"]==scores_df_merged["scores"].min()]["degrees"]

    # plot a vertical line
    fig.add_vline(x=int(degree_best_performance), line_width=3, line_dash="dash", line_color="red", name="Best Performance")

    # update plot layout
    fig.update_layout(
        font=dict(
            family="Arial",
            size=18,  # Set the font size here
            color="Black"
        ),
        xaxis_title="Degree",
        yaxis_title="Mean Absolute Error",
        showlegend=True,
        width=1200,
        height=400,
    )

    fig['data'][0]['line']['color']="grey"
    fig['data'][1]['line']['color']="black"

    fig.show()
    return degree_best_performance

我意识到这个简单的二维示例并不是一个具有代表性的例子,因为仅凭一个属性无法准确预测目标变量。然而,我们来看看转化是否有任何改变。

首先,让我们尝试使用在线新闻受欢迎度数据集:

degree_best_performance_online_news = plot_cross_val_score_comparison(chosen_dataset="Online News Popularity")

在原始和转化数据上训练的多项式回归模型的表现 — 作者提供的图片

在转化数据上训练的模型表现稍好一些。尽管从 3213 到 3202 的绝对误差差异不是特别大,但这确实表明数据转化对训练过程有积极的影响。

通过绘制转化后的数据并建立模型,我们可以看到,数据被向右移动了。这给了模型更多的“空间”来拟合数据:

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
import numpy as np

def plot_polynomial_regression_model(chosen_dataset, chosen_transformation, degree_best_performance):
    # fig settings
    plt.rcParams.update({'font.size': 16})
    fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(15, 6))

    if chosen_dataset == "Online News Popularity":
        df = pd.read_csv("../input/uci-online-news-popularity-data-set/OnlineNewsPopularity.csv")
        y_column = " shares"
        X_column = " n_tokens_content"
        X = df[[X_column]]
        y = df[y_column]
    elif chosen_dataset == "World Population":
        df = pd.read_csv("../input/world-population-dataset/WPP2022_TotalPopulationBySex.csv")
        df = df[df["Time"]==2020]
        df["Area"] = df["PopTotal"] / df["PopDensity"]
        y_column = "PopTotal"
        X_column = "Area"
        X = df[[X_column]]
        y = df[y_column]

    #########################################################################
    # Define model
    #########################################################################
    # degree_best_performance was calculated in the cell above
    degree = int(degree_best_performance)
    polynomial_features = PolynomialFeatures(degree=degree, include_bias=False)

    linear_regression = LinearRegression()
    pipeline = Pipeline(
        [
            ("polynomial_features", polynomial_features),
            ("linear_regression", linear_regression),
        ]
    )

    #########################################################################
    # Fit model
    #########################################################################
    pipeline.fit(X, y)

    ######################################################################################
    # Fit model and plot raw features
    ######################################################################################
    reg = pipeline.fit(X, y)
    X_pred = np.linspace(min(X.iloc[:,0]), max(X.iloc[:,0]),1000).reshape(-1,1)
    X_pred = pd.DataFrame(X_pred)
    X_pred.columns = [X.columns[0]]
    y_pred_1 = reg.predict(X_pred)

    # plot model and transformed data    
    ax[0].scatter(X, y, color='#f3d23aff')
    ax[0].plot(X_pred, y_pred_1, color='black')
    ax[0].set_xlabel(f"{X_column}")
    ax[0].set_ylabel(f"{y_column}")
    ax[0].set_title(f"Raw features / Poly. Regression (degree={degree})", pad=20)

    #########################################################################
    # Fit model with transformed data
    #########################################################################
    def transform_feature(X, chosen_transformation):
        # We add 1 to number_of_words to make sure we don't have a null value in the column to be transformed (0-> -inf) 
        if chosen_transformation == "log":
            return (np.log10(1+ X))
        if chosen_transformation == "box-cox":
            return stats.boxcox(X+1, lmbda=0)

    X_trans = transform_feature(X, chosen_transformation)

    # fit model with transformed data
    reg = pipeline.fit(X_trans, y)

    # define X_pred
    X_pred = np.linspace(min(X_trans.iloc[:,0]), max(X_trans.iloc[:,0]),1000).reshape(-1,1)
    X_pred = pd.DataFrame(X_pred)
    X_pred.columns = [X.columns[0]]

    # predict
    y_pred_2 = reg.predict(X_pred)

    # plot model and transformed data    
    ax[1].scatter(X_trans, y, color='#f3d23aff')
    ax[1].plot(X_pred, y_pred_2, color='black')
    ax[1].set_xlabel(f"{chosen_transformation} Transformed {X_column}")
    ax[1].set_ylabel(f"{y_column}")
    # calc cross val score and add to title
    ax[1].set_title(f"Transformed features  / Poly. Regression (degree={degree})", pad=20)

    fig.suptitle(chosen_dataset)
    fig.tight_layout()

我们使用刚刚定义的函数来绘制表现最佳的多项式模型:

plot_polynomial_regression_model(chosen_dataset = "Online News Popularity", chosen_transformation = "log", degree_best_performance=degree_best_performance_online_news)

多项式回归模型: 在线新闻的分享数 — 作者提供的图片

我们来尝试一下世界人口数据集:

degree_best_performance_world_pop = plot_cross_val_score_comparison(chosen_dataset="World Population")

在原始和转化数据上训练的多项式回归模型的表现 — 作者提供的图片

我们可以看到,模型在原始数据集和转化数据集之间的表现差异很大。虽然基于原始数据的模型在低多项式度数时表现显著更好,但在构建高多项式度数的模型时,训练在转化数据上的模型表现更佳。

多项式回归模型: 国家人口 — 作者提供的图片

仅供比较,这是针对原始数据表现最佳的线性模型:

多项式回归模型:国家人口 — 作者图片

4.2 使用 Scipy 的 Box-Cox 函数

另一个非常流行的变换函数是 Box-Cox 函数,它属于幂变换的范畴。

幂变换器是一类参数化变换,旨在将任何数据分布转换为正态分布的数据集,并最小化方差和偏斜。 [Sklearn.org]

为了实现这种灵活的变换,Box-Cox 函数定义如下:

要使用 Box-Cox 函数,我们必须确定变换参数 lambda。如果未手动指定 lambda,变换器将通过最大化似然函数来寻找最佳 lambda。 [Rdocumentation.org] 可以通过函数scipy.stats.boxcox找到合适的实现。

注意:Box-Cox 函数必须用于大于零的数据。

Box-Cox 变换比对数变换更灵活,因为它可以产生各种变换形状,包括线性、二次和指数,这些可能更好地适应数据。此外,Box-Cox 变换可以用于变换既正偏又负偏的数据,而对数变换只能用于正偏数据。

lambda 参数的用途是什么?

参数 lambda(λ)可以调整,以根据被分析数据的特征来定制变换。lambda 值为 0 会产生对数变换,而 0 到 1 之间的值会产生越来越“”的变换。如果 lambda 设置为 1,则函数不会对数据进行任何变换。

为了展示 Box-Cox 函数可以转换不仅仅是右偏的数据,我还使用了来自波士顿住房数据集的AGE("建于 1940 年前的自有住房比例")属性。

import seaborn as sns

plot_transformer(chosen_dataset = "Housing Data", chosen_transformation = "box-cox", chosen_feature = None, box_cox_lambda = 2.5)

5. 归一化 / 标准化

归一化和标准化是机器学习中的重要预处理步骤。它们可以帮助算法更快地收敛,甚至提高模型的准确性。

5.1 使用 Scikit-learn 进行归一化和标准化

  • Scikit-learn 的 MinMaxScaler将特征缩放到给定范围内。它通过将每个特征缩放到 0 到 1 之间的给定范围来变换特征。

  • Scikit-learn 的 StandardScaler将数据转换为均值为 0 和标准差为 1。

# data normalization with sklearn
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

plt.rcParams['font.size'] = '16'
sns.set_style("darkgrid", {"axes.facecolor": ".9"})

# load data set
census_income = pd.read_csv(r'../input/income/train.csv')

X = census_income[["age"]]

# fit scaler and transform data
X_norm = MinMaxScaler().fit_transform(X)
X_scaled = StandardScaler().fit_transform(X)

# plots
fig, (ax1, ax2, ax3) = plt.subplots(3)
fig.suptitle('Normalizing')
fig.set_size_inches(18.5, 10.5)

# subplot 1 - raw data
ax1.hist(X, 25, facecolor='black', ec="white")
ax1.set_xlabel("Age")
ax1.set_ylabel("Frequency")

# subplot 2 - normalizer
ax2.hist(X_norm, 25, facecolor='black', ec="white")
ax2.set_xlabel("Normalized Age")
ax2.set_ylabel("Frequency")

# subplot 3 - standard scaler
ax3.hist(X_scaled, 25, facecolor='black', ec="white")
ax3.set_xlabel("Normalized Age")
ax3.set_ylabel("Frequency")

fig.tight_layout()

6. 特征交叉

特征交叉是将数据集中的多个特征连接起来以创建新特征的过程。这可以包括结合其他来源的数据,以强调现有的相关性。

创造力没有限制。首先,通过正确组合属性使已知相关性显现是有意义的,例如:

示例 1:预测公寓的价格

假设我们有一系列要出售的公寓,并且有技术蓝图,从而可以获得这些公寓的尺寸。

为了确定公寓的合理价格,具体的尺寸可能不像总面积那样重要:一个房间是 6 米 * 7 米还是 5.25 米 * 8 米,并不像房间有 42 平方米那样重要。如果我只知道尺寸 a 和 b,那么将面积作为一个特征添加进去是有意义的。

预测公寓的价格 — 图片来自作者

示例 2:利用技术知识 — 预测铣床的能耗

几年前,我在研究一个回归模型,该模型可以预测铣床的能耗。

在为特定领域实施解决方案时,查看现有文献总是值得的。由于人们对计算切割机功率需求的兴趣已有 50 年,我们可以利用这 50 年的研究成果。

即使现有的公式仅适用于粗略计算,我们仍可以利用关于属性和目标变量(能耗)之间已识别的相关性的知识。在下图中,你可以看到一些功率需求与变量之间的已知关系。

→ 通过将这些已知关系作为特征突出显示,我们可以使模型更容易处理。例如,我们可以交叉切割宽度 b切割深度 h(以计算截面积 A),并将其定义为一个新特征。这可能有助于训练过程,特别是当我们使用较简单的模型时。

铣床功率需求的计算 — 图片来自作者

但我们不仅仅用它来准备数据集,一些算法将特征交叉作为其工作原理的基本部分。

一些机器学习方法和算法默认已经使用特征交叉

两种著名的机器学习技术使用特征交叉是多项式回归核技巧(例如,与支持向量回归结合使用)

6.1 多项式回归中的特征交叉

Scikit-learn 不包含多项式回归的功能,我们从以下步骤创建一个管道:

  1. 特征变换步骤和

  2. 一个线性回归模型构建步骤。

通过组合和指数化特征,我们生成了若干新的特征,这使得可以表示输出变量之间的非线性关系。

假设我们要构建一个具有二维输入矩阵 X 和目标变量 y 的回归模型。除非特别定义,否则特征转换函数(sklearn.PolynomialFeatures)将矩阵转换如下:

你可以看到,新矩阵包含四个新列。属性不仅被增强,还相互交叉:

import numpy as np
from sklearn.preprocessing import PolynomialFeatures

# Raw input features
X = np.arange(6).reshape(3, 2)
print("Raw input matrix:")
display(X)

# Crossed features
poly = PolynomialFeatures(2)
print("Transformed feature matrix:")
poly.fit_transform(X)

这样我们可以建模任何可想象的关系,我们只需选择正确的多项式度数。

以下示例展示了一个多项式回归模型(多项式度数=5),它基于属性 LSTAT(低社会经济状态的人口百分比)预测波士顿自有住房的中位数价值。

from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline

import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

plt.rcParams['font.size'] = '16'
sns.set_style("darkgrid", {"axes.facecolor": ".9"})

# load data set: Boston Housing
boston_housing_df = pd.read_csv("../input/housing-data-set/HousingData.csv")
boston_housing_df = boston_housing_df.dropna()
boston_housing_df = boston_housing_df.sort_values(by=["LSTAT"])

# define x and target variable y
X = boston_housing_df[["LSTAT"]]
y = boston_housing_df["MEDV"]

# fit model and create predictions
degree = 5
polynomial_features = PolynomialFeatures(degree=degree)
linear_regression = LinearRegression()
pipeline = Pipeline(
    [
        ("polynomial_features", polynomial_features),
        ("linear_regression", linear_regression),
    ]
)
pipeline.fit(X, y)

y_pred = pipeline.predict(X)

# train linear model
regr = LinearRegression()
regr.fit(X,y)

# figure settings
fig, (ax1) = plt.subplots(1)
fig.set_size_inches(18.5, 6)

ax1.set_title("Boston Housing: Median value of owner-occupied homes")
ax1.scatter(X, y, c="black")
ax1.plot(X, y_pred, c="red", linewidth=4, label=f"Polynomial Model (degree={degree})")
ax1.set_ylabel(f"MEDV")
ax1.set_xlabel(f"LSTAT")
ax1.legend()

fig.show()

6.2 特征交叉和核技巧

另一个特征交叉已经被固有使用的例子是核技巧,例如在支持向量回归中。通过核函数,我们将数据集转换到更高维空间,使得可以轻松分离不同类别。

对于以下示例,我生成了一个二维数据集,这在二维空间中相当难以分离,至少对于线性模型而言。

from sklearn.datasets import make_circles
from sklearn import preprocessing
import plotly_express as px
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

sns.set_style("darkgrid", {"axes.facecolor": ".9"})
plt.rcParams['font.size'] = '30'

# generate a data set
X, y = make_circles(n_samples=1_000, factor=0.3, noise=0.05, random_state=0)
X = preprocessing.scale(X)

X=X[500:]
y=y[500:]

# define target value, here: binary classification, class 1 or class 2
y=np.where(y==0,"class 1","class 2")

# define x1 and x2 of a 2-dimensional data set
x1 = X[:,0]
x2 = X[:,1]

import plotly_express as px
# define the kernel function
kernel = x1*x2 + x1**2 + x2**2

circle_df = pd.DataFrame(X).rename(columns={0:"x1", 1:"x2"})
circle_df = circle_df.assign(y=y)

color_discrete_map = {circle_df.y.unique()[0]: "black", circle_df.y.unique()[1]: "#f3d23a"}
px.scatter(circle_df, x="x1", y="x2", color="y", color_discrete_map = color_discrete_map, width=1000, height=800)

# plot the data set together with the kernel value in a 3-dimensional space
color_discrete_map = {circle_df.y.unique()[0]: "black", circle_df.y.unique()[1]: "grey"}
fig = px.scatter(circle_df, x="x1", y="x2", color="y", color_discrete_map = color_discrete_map, width=1000, height=600)
fig.update_layout(
    font=dict(
        family="Arial",
        size=24,  # Set the font size here
        color="Black"
    ),
    showlegend=True,
    width=1200,
    height=800,
)

通过使用核技巧,我们生成了第三维度。在这个例子中,核函数定义为:

import plotly_express as px

# define the kernel function
kernel = x1*x2 + x1**2 + x2**2

kernel_df = pd.DataFrame(X).rename(columns={0:"x1", 1:"x2"})
kernel_df = kernel_df.assign(kernel=kernel)
kernel_df = kernel_df.assign(y=y)

# plot the data set together with the kernel value in a 3-dimensional space
color_discrete_map = {kernel_df.y.unique()[0]: "black", kernel_df.y.unique()[1]: "grey"}  
px.scatter_3d(kernel_df, x="x1", y="x2", z="kernel", color="y", width=1000, height=600)

核技巧:二维数据集转移到三维空间 — 图片由作者提供

三维空间允许使用简单的线性分类器分离数据。

7. 主成分分析(PCA)

主成分分析,或 PCA,通过创建一组从原始特征中衍生的新特征来减少数据集的维度。因此,类似于哈希,我们减少了数据集的复杂性,从而减少了计算工作量。

此外,它还可以帮助我们可视化数据集。具有多个维度的数据集可以在较低维度的表示中进行可视化,同时尽可能保留原始变异。

我将省略如何在本节中深入解释,因为已经有一些很好的资料,例如:

简要总结,以下是四个最重要的步骤:

  1. 标准化数据: 从每个特征中减去均值,并将特征缩放到单位方差。

  2. 计算标准化数据的协方差矩阵: 该矩阵将包含所有特征之间的成对协方差。

  3. 计算协方差矩阵的特征向量和特征值: 特征向量确定新特征空间的方向,并表示主成分,特征值确定它们的大小。

  4. 选择主成分: 选择前几个主成分(通常是特征值最高的那些),并用它们将数据投影到低维空间。你可以根据想要保留的方差量或希望将数据减少到的维度数来选择保留的成分数。

PCA 如何工作 — 图片由作者提供(灵感来源于 [Zheng, Alice, and Amanda Casari, 2018])

我将向你展示如何使用 PCA 创建新的特征,使用鸢尾花数据集

数据集:鸢尾花数据集 [许可证:CC0: 公共领域]

archive.ics.uci.edu/ml/datasets/iris

www.kaggle.com/datasets/uciml/iris

鸢尾花数据集包含关于三种鸢尾花(Iris setosa、Iris virginica 和 Iris versicolor)的信息。它包括花的萼片和花瓣的长度和宽度(以厘米为单位),并包含 150 个观察值。

所以首先,让我们加载数据集,查看四个维度(萼片长度、萼片宽度、花瓣长度、花瓣宽度)中的数据分布。

from matplotlib import pyplot as plt
import numpy as np
import seaborn as sns
import math

import pandas as pd

# Load the Iris dataset
iris_data = pd.read_csv(r'../input/iris-data/Iris.csv')
iris_data.dropna(how="all", inplace=True) # drops the empty line at file-end

sns.set_style("whitegrid")
colors = ["black", "#f3d23aff", "grey"]
plt.figure(figsize=(10, 8))

with sns.axes_style("darkgrid"):
    for cnt, column in enumerate(iris_data.columns[1:5]):
        plt.subplot(2, 2, cnt+1)
        for species_cnt, species in enumerate(iris_data.Species.unique()):
            plt.hist(iris_data[iris_data.Species == species][column], label=species, color=colors[species_cnt])

        plt.xlabel(column)
    plt.legend(loc='upper right', fancybox=True, fontsize=8)

    plt.tight_layout()
    plt.show()

从分布中你可以看出,基于花瓣宽度和花瓣长度可以相对较好地区分各个类别。在其他两个维度中,分布强烈重叠。

这表明花瓣宽度和花瓣长度对我们的模型可能比其他两个维度更为重要。因此,如果我只能使用两个维度作为模型特征,我会选择这两个。如果我们绘制这两个维度,我们得到如下图:

还不错,我们看到通过选择“最重要”的属性,我们可以减少维度并尽可能少地丢失信息。

PCA 更进一步,它对数据进行中心化和缩放,并将数据投影到新生成的维度上。这样我们不再局限于现有维度。类似于我们在空间中旋转数据集的 3D 表示,直到找到一种使我们尽可能容易分离类别的方向:

import plotly_express as px

color_discrete_map = {iris_data.Species.unique()[0]: "black", iris_data.Species.unique()[1]: "#f3d23a", iris_data.Species.unique()[2]:"grey"}

px.scatter_3d(iris_data, x="PetalLengthCm", y="PetalWidthCm", z="SepalLengthCm", size="SepalWidthCm", color="Species", color_discrete_map = color_discrete_map, width=1000, height=800)

鸢尾花数据集在 3D 图中的可视化 — 图片由作者提供

总的来说,PCA 会变换、缩放和旋转数据,直到找到一个新的维度集(主成分),该维度集捕捉到原始数据中最重要的信息。

适用于数据集的 PCA 函数可以在 Scikit-learn 中找到,sklearn.decomposition.PCA

7.1 使用 Scikit-learn 的 PCA

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
import matplotlib
import plotly.graph_objects as go

# Load the Iris dataset
iris_data = pd.read_csv(r'../input/iris-data/Iris.csv')
iris_data.dropna(how="all", inplace=True) # drops the empty lines

# define X
X = iris_data[["PetalLengthCm", "PetalWidthCm", "SepalLengthCm", "SepalWidthCm"]]

# fit PCA and transform X
pca = PCA(n_components=2).fit(X)
X_transform = pca.transform(X)
iris_data_trans = pd.DataFrame(X_transform).assign(Species = iris_data.Species).rename(columns={0:"PCA1", 1:"PCA2"})

# plot 2d plot
color_discrete_map = {iris_data.Species.unique()[0]: "black", iris_data.Species.unique()[1]: "#f3d23a", iris_data.Species.unique()[2]:"grey"}
fig = px.scatter(iris_data_trans, x="PCA1", y="PCA2", color="Species", color_discrete_map = color_discrete_map)

fig.update_layout(
    font=dict(
        family="Arial",
        size=18,  # Set the font size here
        color="Black"
    ),
    showlegend=True,
    width=1200,
    height=800,
)

如果你将前两个主成分(PC1 和 PC2)的图与花瓣宽度和花瓣长度的二维图进行比较,你会发现数据集中最重要的信息得到了保留。此外,数据经过了中心化和标准化。

转换后的鸢尾花数据的 PCA1 和 PCA2 — 作者提供的图像

总结

文章中描述的技术可以应用于任何类型的数据,但它们不能替代特定领域的专用方法。

一个很好的例子是声学领域。假设你对声学和信号处理一无所知…。

  • 你会如何处理空气中的声音信号?你会提取哪些特征?

你是否曾考虑将信号分解为单独的频谱成分以了解信号的组成?可能不会。幸运的是,早在你之前,就有人提出了同样的问题,并定义了一些合适的变换方法,比如快速傅里叶变换(FFT)。

站在巨人的肩膀上

即使技术已经很先进,这也不意味着我们不能利用几十年前的洞见。我们可以从这些过去的经验中学习,并用它们来提高模型构建过程的效率。因此,如果你想创建有效解决实际问题的模型,花些时间了解数据来源领域总是一个好主意。

继续阅读…

如果你喜欢阅读,并想继续了解机器学习的概念、算法和应用,你可以在这里找到我相关的文章列表:

多米尼克·波尔泽

多米尼克·波尔泽

机器学习:概念、技术和应用

查看列表5 个故事

如果你对注册 Medium 以获取所有故事的无限访问权限感兴趣,可以通过使用我的 推荐链接来支持我。这将为我带来一小部分佣金,而不会给你带来额外费用。

感谢阅读!

参考文献

Box, G E P 和 D R Cox. “变换分析。” : 43.

Brown, Sara. 2022 年。 “为什么是‘数据中心人工智能’的时机。” MIT Sloanmitsloan.mit.edu/ideas-made-to-matter/why-its-time-data-centric-artificial-intelligence(2022 年 10 月 30 日)。

educative.io。 “特征选择和特征工程——机器学习系统设计。” Educative: Interactive Courses for Software Developerswww.educative.io/courses/machine-learning-system-design/q2AwDN4nZ73(2022 年 11 月 25 日)。

H2O 的特征工程——Dmitry Larko,高级数据科学家, H2O.Ai。 2017 年。 www.youtube.com/watch?v=irkV4sYExX4(2022 年 9 月 5 日)。

featureranking.com。 “案例研究:预测收入状况。” [www.featureranking.com*](www.featureranking.com/) www.featureranking.com/tutorials/machine-learning-tutorials/case-study-predicting-income-status/(2022 年 11 月 26 日)。

Geeksforgeeks. 2020 年。 “Python | Box-Cox 变换。” GeeksforGeekswww.geeksforgeeks.org/box-cox-transformation-using-python/(2022 年 12 月 25 日)。

Google Developers. 2017 年。 “使用 TensorFlow 的特征工程入门——机器学习配方第 9 期。” www.youtube.com/watch?v=d12ra3b_M-0(2022 年 9 月 5 日)。

Google Developers. “介绍 TensorFlow 特征列。” developers.googleblog.com/2017/11/introducing-tensorflow-feature-columns.html(2022 年 9 月 21 日)。

Heavy.ai。 2022 年。 “什么是特征工程?定义和常见问题 | HEAVY.AI。” www.heavy.ai/technical-glossary/feature-engineering(2022 年 9 月 19 日)。

heavy.ai。 “特征工程。” www.heavy.ai/technical-glossary/feature-engineering

Koehrsten, Will. “手动特征工程入门。” kaggle.com/code/willkoehrsen/introduction-to-manual-feature-engineering(2022 年 9 月 5 日)。

Kousar, Summer. “网络安全薪资的 EDA。” kaggle.com/code/summerakousar/eda-on-cyber-security-salary(2022 年 9 月 7 日)。

Moody, John. 1988. “在多分辨率层次中的快速学习。” 收录于 神经信息处理系统的进展,Morgan-Kaufmann. proceedings.neurips.cc/paper/1988/hash/82161242827b703e6acf9c726942a1e4-Abstract.html(2022 年 11 月 28 日)。

Poon, Wing. 2022. “机器学习中的特征工程(1/3)。” Medium. towardsdatascience.com/feature-engineering-for-machine-learning-a80d3cdfede6(2022 年 9 月 19 日)。

Poon, Wing. “机器学习中的特征工程(2/3)第二部分:特征生成。”:10。

pydata.org. “Pandas.Get_dummies — Pandas 1.5.2 文档。” pandas.pydata.org/docs/reference/api/pandas.get_dummies.html(2022 年 11 月 25 日)。

Raschka, Sebastian. “主成分分析。” Sebastian Raschka, PhD. sebastianraschka.com/Articles/2015_pca_in_3_steps.html(2022 年 12 月 17 日)。

Rdocumentation.org. “Boxcox 函数 — RDocumentation。” www.rdocumentation.org/packages/EnvStats/versions/2.7.0/topics/boxcox(2022 年 12 月 25 日)。

Sarkar, Dipanjan. 2019a. “分类数据。” Medium. towardsdatascience.com/understanding-feature-engineering-part-2-categorical-data-f54324193e63(2022 年 9 月 19 日)。

Sarkar, Dipanjan. 2019b. “连续数值数据。” Medium. towardsdatascience.com/understanding-feature-engineering-part-1-continuous-numeric-data-da4e47099a7b(2022 年 9 月 19 日)。

Sarkar, Dipanjan, Raghav Bali, 和 Tushar Sharma. 2018. 用 Python 实践机器学习. 加州伯克利:Apress. link.springer.com/10.1007/978-1-4842-3207-1(2022 年 11 月 25 日)。

Siddhartha. 2020. “TensorFlow 特征列(Tf.Feature_column)演示。” ML Book. medium.com/ml-book/demonstration-of-tensorflow-feature-columns-tf-feature-column-3bfcca4ca5c4(2022 年 9 月 21 日)。

Sklearn.org. “6.2. 特征提取。” scikit-learn. scikit-learn/stable/modules/feature_extraction.html(2022 年 11 月 28 日)。

spark.apache.org. “提取、转换和选择特征 — Spark 3.3.1 文档。” spark.apache.org/docs/latest/ml-features(2022 年 12 月 1 日)。

Tensorflow.org. “Tf.One_hot | TensorFlow v2.11.0。” TensorFlow. www.tensorflow.org/api_docs/python/tf/one_hot(2022 年 11 月 25 日)。

联合国经济社会事务部,人口司(2022)。2022 年世界人口展望,在线版

Valohai.com. 2022. “什么是机器学习管道?” valohai.com/machine-learning-pipeline/(2022 年 9 月 19 日)。

沃塔瓦,亚当。2022. “跟上数据 #105。” Medium. adamvotava.medium.com/keeping-up-with-data-105-6a2a8a41f4b6(2022 年 10 月 21 日)。

温伯格,基利安等。2009. “大规模多任务学习的特征哈希。” 收录于 第 26 届国际机器学习大会论文集 — ICML ’09,蒙特利尔,魁北克,加拿大:ACM 出版社,1–8. portal.acm.org/citation.cfm?doid=1553374.1553516(2022 年 12 月 25 日)。

什么使特征变得优秀? — 机器学习食谱 #3。2016. www.youtube.com/watch?v=N9fDIAflCMY(2022 年 9 月 5 日)。

维基媒体。“各国年均温度.png。” commons.wikimedia.org/wiki/File:Average_yearly_temperature_per_country.png(2022 年 12 月 25 日)。

维基百科。2022. “特征哈希。” 维基百科. en.wikipedia.org/w/index.php?title=Feature_hashing&oldid=1114513799(2022 年 11 月 28 日)。

郑阿丽斯和阿曼达·卡萨里。2018. 机器学习特征工程

7 条数据科学家的 Scikit-Learn 最佳实践

原文:towardsdatascience.com/7-scikit-learn-best-practices-for-data-scientists-f232a6ed2756

充分利用此机器学习包的技巧

Aashish NairTowards Data Science Aashish Nair

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

--

图片由 John Schnobrich 拍摄,来自 Unsplash

Scikit Learn 是机器学习领域的常用库,很容易理解原因。这个包由简单但有效的工具组成,并配有非常详尽的文档。

然而,尽管它易于使用,但如果不遵循某些实践,容易犯错,尤其是对于初学者而言。我自己也常常在看到我以前工作中使用该模块时的明显错误时想要捂脸。

最终,即使你严格遵循文档,也很容易错误地遗漏某些关键特性或做出次优决策。

因此,我借鉴了过去的经验,深入探讨了 7 条 Scikit Learn 最佳实践,以有效进行预测数据分析。

1. 使用 Scikit Learn(而不是 Pandas)进行特征工程

Scikit Learn 专为机器学习任务设计,这当然包括特征工程。然而,有些人习惯使用 Pandas 进行某些操作(例如,独热编码),因为这是大多数人最先学习的包。

虽然 Pandas 库在进行探索性数据分析方面表现优秀,但在机器学习领域中,它无法与 Scikit Learn 相比。

Scikit Learn 中的变换器是为机器学习应用设计的。它们可以高效地准备训练集和测试集,同时避免数据泄露(如果操作得当)。

将 Pandas 函数硬塞到与其他 Scikit Learn 工具的数据管道中,必然会导致低效的流程,容易出错。

更好的做法是主要依赖 Scikit Learn 进行与特征工程相关的操作。

2. 在分类任务中使用分层拆分

当感兴趣的数据表现出数据不平衡时,分类任务可能会具有挑战性,即一个或多个类别被低估。

幸运的是,通过分层抽样,用户可以在原始数据的每个子集中保持所有类别的存在。

在将数据集拆分为训练集和测试集时,用户可以使用 stratify 参数。

在将训练数据拆分为多个折叠进行交叉验证时,用户可以使用 StratifiedKFold 类。

3. 使用 n_jobs 参数加速超参数调整

超参数调整可能是数据建模阶段中最耗时的部分之一。逐个评估多个超参数组合自然是一个缓慢的过程。

幸运的是,用户可以通过利用 n_jobs 参数来加速超参数调整方法,如网格搜索和随机搜索,该参数决定了并行运行的作业数量。默认情况下,n_jobs 值设置为 1,但用户可以通过将 n_jobs 设置为 -1 来更快地获得结果,这样可以利用所有可用的处理器并行运行作业。

4. 分配 random_state 值以获得可重复的结果

一些特征工程过程和机器学习算法包含随机性。然而,利用纯随机性的程序将无法重现其结果,这使得进行实验变得困难。

用户可以通过为随机数生成器设置种子来获得可重复的结果。对于 Scikit Learn 工具,可以通过在适用时将值分配给 random_state 参数来完成。这确保了程序将产生可重复的结果。

用户在执行如下操作时可以设置 random_state 值:

  • 将数据集拆分为训练集和测试集

  • 配置机器学习分类器对象

  • 超参数调整

注意:分配给 random_state 参数的数字并不重要,只要在实验过程中不改变它。

5. 在超参数调整中指定评分参数

超参数调整方法通过不同的超参数组合评估模型。这种技术的目的是识别出能够产生最佳性能的超参数。

然而,当模型使用错误的指标进行评估时,如何能保证其表现良好?当你在 Scikit Learn 模块的 GridSearchCV 和 RandomizedSearchCV 对象中使用 scoring 参数的默认值时,这是一种非常可能的结果。

默认情况下,网格搜索和随机搜索通过准确性评估分类模型的超参数。不幸的是,这很少是机器学习应用的合适指标。

为了避免这种情况,请确定最适合目标模型的评估指标,并将其分配给评分参数。可以在包的文档中找到可用指标的列表。

如果提供的指标都不合适,也可以使用 make_scorer 函数创建自定义指标。当用户偏好某种错误类型时,这是一个有用的功能。

6. 使用管道转换数据

刚开始使用 Scikit Learn 的初学者可能习惯一次进行一个变换。这种方法需要在训练集和测试集上多次使用 fittransform 方法。

以这种方式转换数据需要多行代码,并且容易出现错误(例如,在测试集上使用 fit)。因此,你会很高兴知道,Scikit Learn 提供了一个工具,可以更轻松地完成这些操作:管道。

Scikit Learn 管道是一个工具,它将一系列变换和估算器串联在一起,使用户能够执行更易于编写、阅读和调试的操作。

我始终倡导使用管道,并将在此再次提及;它们实在太好用了。

例如,我们可以使用管道对象来执行之前的操作:

与之前的代码片段相比,这里的代码更加可读,容易理解工作流程中的所有步骤。

此外,管道对象中的所有变换和建模都可以通过一个 fit 方法在训练集上执行。此外,相同的变换也可以在生成测试集预测之前通过一个 predict 方法应用。

7. 熟悉与 Scikit Learn 兼容的其他包

最终,Scikit Learn 包的广泛工具范围无法涵盖所有可能的情况。

因此,熟悉与 Scikit Learn 兼容的其他包是值得的。这些包包含可以与 Scikit Learn 一起用于特征工程和数据建模的工具。

两个值得注意的例子是 feature_engineXGBoost 包,它们拥有自己独特的变换器和机器学习算法,可以与其他 Scikit Learn 工具一起使用。

结论

照片由 Prateek Katyal 提供,来源于 Unsplash

Scikit Learn 包具有巨大的实用性,可以用于解决各种机器学习问题。

然而,用户必须学习遵循某些最佳实践,以从包中获得最大收益。

如果你发现这个简短的概述有帮助,你也可以考虑阅读这些文章:

## K-Fold 交叉验证:你做对了吗?

讨论在数据集上执行 k-fold 交叉验证的正确(和不正确)方法

towardsdatascience.com ## 为什么你应该使用 Scikit-Learn 管道

这个工具将你的代码提升到一个新水平

towardsdatascience.com ## 在机器学习中利用随机性

“随机”应该有多随机?

towardsdatascience.com

祝你在数据科学的探索中好运!

你已经成为一个高级 Sklearn 用户的 7 个迹象

原文:towardsdatascience.com/7-signs-youve-become-an-advanced-sklearn-user-without-even-realizing-it-3b7085c600f1

以及一个专业的机器学习工程师…

Bex T.Towards Data Science Bex T.

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

--

图片由我使用 Midjourney 制作

介绍

准备好被惊艳到吧!我将列出 七个 不可否认的迹象,表明你已经成为一个高级的 Sklearn 用户而丝毫未曾察觉。由于 Sklearn 是地球上使用最广泛的机器学习库,你不妨将这些迹象看作是你已经是一个非常有能力的机器学习从业者的标志。

让我们开始吧!

0. 三个分区

我们在机器学习中几乎所做的一切,都是为了避免过拟合。而对抗过拟合的最强工具之一,就是将数据拆分为三部分,而非两部分!

谷歌决策智能部门负责人Cassie Kozyrkov表示,数据拆分是机器学习中最强大的理念,你也同意她的观点。

你知道过拟合不仅会发生在训练集上,还可能出现在验证集上。你已经观察到,使用相同的数据集进行测试和超参数调优,常常会引入微妙的数据泄露。

通过不断调整超参数,基于模型在特定测试集上的表现,有可能使模型对该特定数据集过拟合。

所以,你使用 50% 的可用数据训练所选模型。接着,你使用包含 25% 数据的独立验证集对模型进行微调和评估。最后,当你的模型准备好投入实际使用时,你会用一个完全未接触过的、原始的(我指的是你甚至没有查看前五行)测试集进行最后一次测试。

牢记这一规则,你已经将这段代码片段保存在桌面上,以便随时复制粘贴:

from sklearn.model_selection import train_test_split

def split_dataset(data, target, train_size=0.5, random_state=42):
    # Splitting the dataset into training set and remaining data
    X_train, remaining_data, y_train, remaining_target = train_test_split(
        data, target, train_size=train_size, random_state=random_state
    )

    # Splitting the remaining data equally into test and validation sets
    X_val, X_test, y_val, y_test = train_test_split(
        remaining_data, remaining_target, test_size=0.5, random_state=random_state
    )

    return X_train, X_val, X_test, y_train, y_val, y_test

SYDD!如果说未经审视的生活不值得过,那么这里有四个字供你遵循:拆分你该死的数据。 — Cassie Kozyrkov

## 数据科学中最强大的理念

编辑描述

数据科学中最强大的理念

1. 设置常识基线

如何判断自己在机器学习项目中所做的任何事情是否对最终产品——完美模型有贡献?

在这里,“一见钟情”的方法永远不够。那个人需要一个北极星,能随时参考以判断他们刚训练的模型是否能通过即使是最简单的测试。

在你的项目中,你一次又一次地看到,这一关键的北极星是常识基线性能。

一旦确定了机器学习任务、选择了特征并定义了目标变量,你可以拟合DummyRegressorDummyClassifier来评估随机猜测模型在你特定问题上的表现。这一得分作为基线,你所有后续的实验都旨在提高这一初始结果。

from sklearn.dummy import DummyClassifier

clf = DummyRegressor().fit(X_train, y_train)
clf.score(X_test, y_test)

你也总是不厌其烦地向他人推荐这一做法,包括你的好友 Bex。

他曾经愚蠢地尝试了所有适合的 Sklearn 模型和越来越复杂的深度学习架构用于图像分类任务,以弄清楚为什么所有实验都无法超过某个阈值。

你指出 Bex 缺少基线,当他尝试DummyClassifier时,他意识到整个任务是不可解的,他的所有努力都浪费了,因为没有一个实验能够超越随机猜测模型。他不得不寻找新的数据集。

没有什么比过度工程的恐怖更让人不安了,完全相信你的模型是卓越的,结果却发现它仅仅和一个盲人投掷飞镖的猴子一样有效。

你给你的朋友上了一堂宝贵的课。

2. 特征选择与工程

当面对一个拥有几十甚至数百个特征的数据集时,你会注意到你的宝贵时间和硬件资源。与其盲目地在所有现有特征上测试模型,你会退一步,尝试隔离出最有潜力的特征。

首先,你可以逐一查看特征,使用VarianceThreshold去除那些方差不显著的特征。然后,你可以通过分析特征之间的关系来查看大局,从中决定哪些特征是值得保留的。

为了帮助你做出决定,你可以使用诸如成对相关系数这样的指标,或者更好的是,采用基于模型的方法,使用如RFECV(递归特征消除与交叉验证)或SelectFromModel的类。

## 3 步特征选择指南:提升模型的表现

编辑描述

## 3 步特征选择指南:提升模型的表现

随后,你施展你的特征工程魔法,以最大限度地发挥最佳特征的作用。在这里,你对数值变换(如标准化、归一化和对数变换)之间复杂差异的了解将是关键。

你对StandardScalerMinMaxScalerQuantileTransformerPowerTransformer等的权衡了如掌中之物。

## 如何区分缩放、标准化和对数变换

编辑描述

## 如何区分缩放、标准化和对数变换

在处理缺失值时,你不仅仅停留在SimpleImputer的均值/众数插补等简单策略上。你对基于模型的高级插补技术如KNNImputerIterativeImputer有着深刻的理解,并能够根据缺失值的类型选择最合适的方法。

## Sklearn 高级缺失数据插补方法深入教程

编辑描述

## 高级缺失数据插补方法教程:使用 Sklearn

对于分类特征,你使用多种技术来正确编码它们,如OrdinalEncoderOneHotEncoder。当使用后一种方法时,你会非常小心虚拟变量陷阱,这可能会在数据集中引入多重共线性。

尽管我只是一个接一个地列出了特征选择和特征工程,你这位经验丰富的专家知道不应将两者锁定在一个固定的顺序中。你在两者之间迭代,反复试验和完善你的方法。

3. 模型选择

这时,你展示了你所参加的众多在线课程或你华丽的教育背景并未白费。

你以完全的冷静和自信直观地选择最适合给定任务的算法和模型架构。在这里,你的目标并非总是选择性能最好的模型,而是选择在现有约束下最适合问题的模型。

这些约束在你的大脑中像五个巨大的障碍:

  1. 计算资源:如果硬件有限,你选择更简单和更小的模型,即使这意味着性能下降。

  2. 时间限制:当森林模型和深度学习模型对你的时间和预算施加过大压力时,你倾向于选择旧的线性模型或浅层树。

  3. 模型复杂度与性能权衡:你在强大预测能力的模型(但可能过拟合)和简单模型(可能欠拟合)的之间找到平衡。

  4. 可解释性:当模型解释性和利益相关者信心至关重要时,你优先考虑线性模型或树模型。

  5. 数据集大小:你根据数据的大小指导模型选择。一些模型在大型数据集上表现出色,比如深度学习模型,而 SVM 等模型更适合小型数据集。

模型选择是一门艺术,你也如此对待它。

4. 战略性交叉验证

如果说有什么比将数据分成三份更让你喜欢的,那就是交叉验证。每当你的时间和资源限制允许时,你总是尝试使用适当的 CV 策略。

你非常清楚,交叉验证(CV)是提供最可靠模型性能估计的方法,使其成为你在应对过拟合或欠拟合时最宝贵的工具。

除了简单的 KFold CV,你还利用其他强大的策略,例如:

  1. StratifiedKFold - 用于分类任务。

  2. RepeatedKFold - 当你特别关注模型性能的准确估计时。

  3. ShuffleSplit - 用于更好地控制训练/测试分割。

  4. GroupKFold - 用于非 IID 数据(独立同分布)。

  5. TimeSeriesSplit - 用于时间序列数据。

## 一站式教程:你可以(应该)使用的所有交叉验证技术

编辑描述

towardsdatascience.com

在你的交叉验证工作中,Sklearn 管道占据了重要位置。你将预处理和建模结合成一个单一的管道,而不是在 CV 分割器外部执行预处理和内部建模。

管道是你防止数据泄露的额外保障,允许你编写可读的、模块化的代码,其他人也很乐意阅读。

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

编辑描述

towardsdatascience.com

5. 对随机性的完全控制

你不和随机性开玩笑。你从经验中学到,紧紧控制伪随机生成器可以节省你几个小时的时间,具体表现为:

  • 可重复性

  • 调试和测试

  • 模型性能比较

  • 超参数调整

你也很自豪地知道各种播种方法的细节:

  1. random_state=None - Sklearn 使用全局 NumPy 种子 np.random.seed(seed_number) 作为默认行为。在这种情况下,每次调用涉及随机性的函数,如fittrain_test_splitsplit、CV 类等,都会产生不同的结果。

  2. random_state=integer - 调用依赖于随机性的函数(如上所述)总是会产生相同的结果,前提是使用相同的整数种子。你有你最喜欢的种子(我的是 1121218)。

  3. random_state=np.random.RandomState(一个 RNG 实例) - 这种播种方法提供了最稳健的 CV 结果,尽管传递整数被认为更安全且更可取。它每次运行都会返回不同的结果。

你还在 iPhone 上设置了每周提醒,以便阅读 控制随机性 部分,以免这些细节从记忆中泄漏。

6. 不使用 Sklearn 进行超参数调整

你尊敬 Sklearn,因为它引入了HalvingGridSearchHalvingRandomSearch来修复比乌龟还慢的GridSearch,但在你当前的工作流程中,你友好地转向其他工具。

像今天的酷孩子一样,你的首选是 Optuna。它更具 Python 风格,更智能,并提供了 Sklearn 甚至不会考虑添加的大量小玩意。

你最喜欢的 Optuna 特性包括:

  • 与各种框架的无缝集成:Jupyter、Sklearn、XGBoost、CatBoost、TensorFlow、PyTorch,等等。

  • 先进的调整算法,名字你甚至发音不出来。

  • 在调整过程开始之前,自动排除不靠谱的超参数。

  • 更具 Python 风格且更易读的参数网格。

  • 可视化和分析功能,包括参数重要性图、平行坐标图和优化历史图。

  • 分布式计算以利用多个核心。

很可能,你最喜欢的 Optuna 教程是:

[## 为什么 Kaggle 上的每个人都对 Optuna 在超参数调整中的狂热?

编辑描述

towardsdatascience.com

7. 在另一个层次上的模型评估

就像你擅长烹饪主菜一样,你在准备甜点——模型评估方面也表现出色。

在你的一生中,你无数次调用过:

import sklearn

sklearn.metrics.SCORERS.keys()

查看你测量模型表现的所有选项。你不满足于从这个列表中选择一个指标,而是倾向于选择多个指标,从不同角度评估和优化你的模型。

多类别分类指标的综合指南

编辑描述

towardsdatascience.com

在这方面你最大的优势是区分所有疯狂的分类指标及其根据所选分类器是OVO还是OVR而变化的微妙方式。

你也曾因为将这些图片设置为手机的自动更换壁纸而被称为怪咖几次(但你一点也不在意 :)。

图片由我提供

为了更准确地展示你模型的表现,你努力避免炫耀,使用了以下一些可视化工具:

Sklearn 用户指南的截图。BSD-3 条款许可证。

结论

尽管标题和文章在承认老 Sklearn 高手的同时有一种玩笑的语气,但我内在的意图是为初学者提供指导。我想弥合廉价在线课程中流行的陈旧的复制粘贴知识与只能通过几个月的实际操作和经验获得的实用知识和专业技能之间的差距。

我希望这篇文章不仅为你提供了灵感,还带来了一点娱乐,帮助你深入探索 Sklearn 并成为它的高手!

感谢阅读!

喜欢这篇文章和它那古怪的写作风格?想象一下,能够访问更多类似的文章,所有这些都由一个聪明、迷人、机智的作者(顺便说一句,就是我 😃)编写。

仅需 4.99 美元的会员,你将不仅能访问我的故事,还能获得 Medium 上最聪明、最杰出的头脑的宝贵知识。如果你使用我的推荐链接,你将获得我超新星般的感激和一个虚拟的高五,以支持我的工作。

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

获取所有⚡高级⚡内容的独家访问权限,无限制地浏览 Medium。通过购买我支持我的工作……

ibexorigin.medium.com

图片由我使用 Midjourney 制作

7 步骤帮助你让 Matplotlib 条形图更美观

原文:towardsdatascience.com/7-steps-to-help-you-make-your-matplotlib-bar-charts-beautiful-f87419cb14cb

通过简单的调整轻松提升 Matplotlib 数据可视化的质量

Andy McDonaldTowards Data Science Andy McDonald

·发布于 Towards Data Science ·13 分钟阅读·2023 年 3 月 27 日

--

Matplotlib 的水平条形图,在更改了多个特征后,使其更具视觉吸引力。图片来源于作者。

条形图是一种常用的数据可视化工具,其中分类特征由长度/高度不同的条形表示。条形的高度或长度对应于该类别所表示的值。

条形图可以在 matplotlib 中轻松创建。然而, matplotlib 库通常被认为是生成不令人兴奋的图表的库,并且使用起来可能具有挑战性。然而,通过坚持、探索以及几行额外的 Python 代码,我们可以生成独特、美观且信息量丰富的图形。

如果你想了解 Matplotlib 在稍加努力后的能力,你可能会对查看我之前的文章感兴趣:

## 3 个你没想到是用 Matplotlib 创建的独特图表

利用 Python 的 Matplotlib 创建高级数据可视化

towardsdatascience.com

在本文中,我们将探讨如何将一个无聊的图表转变为:

Matplotlib 的水平条形图显示了不同储层区间的孔隙度值。图片来源于作者。

变成这样:

在将条形图从一个平淡的图形转变为更具视觉吸引力的图形之前和之后的对比。作者提供的图片。

我们将看到如何通过几行简单的 Python 代码来改善我们试图讲述的故事。

教程的视频版本

如果你想查看如何从零开始构建这段代码的视频形式,你可能会对以下内容感兴趣:

导入库并设置数据

第一步是导入我们将要使用的库。在这种情况下,我们将使用 pandas 来存储数据,并使用 matplotlib 来创建图形。

import pandas as pd
import matplotlib.pyplot as plt

接下来,我们将创建一些数据,这些数据来源于 Xeex Force 2020 Lithology Machine Learning competition。这些数据表示单个井,包含 Hugin Fm 内砂岩岩性平均孔隙度值。这些井起源于挪威大陆架。

请参阅文章底部以获取有关此数据集的更多详细信息。

我们可以通过一个简单的字典来创建数据,而不是从 CSV 文件中加载,并将其传递给 pd.DataFrame() 函数。

wells_porosity = {'15/9-13': 18.2, '16/10-1': 26.0, 
 '16/10-2': 21.8, '16/10-3': 16.7, '16/2-16': 19.8,
 '25/2-13 T4': 13.3, '25/2-14': 11.6, '25/2-7': 10.7, 
 '25/3-1': 6.9, '25/4-5': 12.0, '25/5-1': 8.9,
 '25/5-4': 15.0, '25/6-1': 18.9, '25/7-2': 6.5, 
 '25/8-5 S': 21.2, '25/8-7': 26.1, '25/9-1': 23.0,
 '26/4-1': 13.9}

df = pd.DataFrame(wells_porosity.items(), columns=['well', 'porosity'])

使用 Matplotlib 创建基本条形图

现在我们已经设置好 pandas 数据框,我们可以开始创建我们的第一个条形图。创建条形图有几种方法,其中一种是直接使用数据框(df.plot(kind='bar'....)),然而,在本文中,我们将专注于使用以 matplotlib 为中心的代码来构建我们的图表。

要使用 matplotlib 创建一个基本的条形图,我们首先需要设置我们的 fig 和 ax 变量,这些变量将被设置为 plt.subplots()。在此函数中,我们可以传入图形大小。

接下来,我们将创建一个名为 bars 的新变量,并将其分配给 plt.bar()。在此函数中,我们可以简单地传入我们的分类变量,此处为井名称列表,以及平均孔隙度值。

fig, ax = plt.subplots(figsize=(8,8))

bars = plt.bar(df['well'], df['porosity'])

plt.show()

当运行时,我们会看到如下的条形图。正如你所见,它非常基础,也不太吸引人。

使用 matplotlib 生成的基本条形图。作者提供的图片。

如果我们仔细查看图表,会发现更多问题:

  • 阅读 x 轴上的标签很困难

  • 我们需要更加动脑筋来理解每个条形的值

  • 比较条形图是困难的

让我们看看如何创建一个更有效、更具美感的可视化。

1. 旋转你的图表

改善条形图的第一步是将其旋转 90 度。

这使得阅读像我们这样较长的标签变得更容易。我们还可以考虑旋转 x 轴上的标签,但这需要读者也倾斜头部来尝试阅读它们。

此外,水平条形图是在报告或演示文稿中节省空间的绝佳方式,同时保持可读性。如果你有大量类别,这尤其有用。

要旋转我们的条形图,我们需要将 matplotlib 中调用的图表类型从 .bar() 改为 .barh()

fig, ax = plt.subplots(figsize=(8,8))

bars = plt.barh(df['well'], df['porosity'])

plt.show()

我们得到的图表是带有类别标签(井名)的格式更加美观和易读。

我们现在可以明确哪一根条形属于哪个井。

Matplotlib 水平条形图显示不同储层区间的孔隙度值。图片由作者提供。

2. 按顺序排列条形

改进图表的下一步是将条形按升序排序。这可以显著提高图表的可读性。

在对数据进行任何排序之前,你需要首先考虑这是否是一个明智的选择。

如果你的条形与应按特定顺序排列的类别相关,那么按从长到短排序可能不是最佳选择。例如,星期几、月份或年龄组。

将条形按从长到短排序,可以使条形图更易于阅读,允许读者轻松比较不同的条形。当条形长度相近时尤为如此。

这还通过赋予数据一种顺序感,使图表看起来更加美观。

要排序数据,我们需要返回到数据框中,并按 porosity 排序值。

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,8))

bars = plt.barh(df['well'], df['porosity'])
plt.show()

当我们运行上述代码时,我们得到如下图表。

Matplotlib 水平条形图显示不同储层区间的孔隙度值按降序排列。图片由作者提供。

3. 移除边框和坐标轴

如果我们有多余的图表元素,如网格线和边框(通常称为“图表垃圾”),它们可能会分散读者的注意力,并且图表的理解可能需要更长时间。

我们可以去除这些多余的图表垃圾,从而不仅改善图表的可读性,还能提高图表的美观性以及我们想传达的信息。

对于我们的图表,我们将通过调用 ax.spines[[‘right’, ‘top’, ‘bottom’]].set_visible(False) 来移除图表的顶部、底部和右侧边缘。

我们还将隐藏 x 轴。你可能会想,为什么我们要移除坐标轴上的数字——这不是会影响可读性吗?

这是正确的,然而,我们将在下一步中看到如何让读者更容易理解这些值。

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,8))

bars = plt.barh(df['well'], df['porosity'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

plt.show()

当我们运行上述代码时,得到如下图表。

现在看起来比前一步干净多了。

去除多余图表垃圾后的 Matplotlib 水平条形图。图片由作者提供。

4. 添加数据标签

在上述图片中,我们移除了 x 轴的刻度和数字。这确实减少了可读性,然而,如果 x 轴保留,我们期望读者在尝试理解绝对值并比较不同条形时需要额外的工作。

为了使图表更有效,我们可以为每个条形添加绝对值数据标签。这提高了清晰度,节省了空间,并提高了精确性。

为了更方便地在条形图中添加标签,matplotlib 的开发者引入了 [bar_label()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.bar_label.html) 函数。这允许我们在条形图中自动添加标签。

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,8))

bars = plt.barh(df['well'], df['porosity'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

ax.bar_label(bars)

plt.show()

当运行上述代码时,我们得到如下图表。我们可以直接在条形的末端看到绝对值,这显著提高了可读性。例如,如果仅根据条形长度来看前两个条形,我们会说它们是一样的,但如果查看绝对值,我们会发现它们略有不同。

Matplotlib 水平条形图中显示了数据标签,以提高可读性。图片由作者提供。

控制 matplotlib 的 bar_label 函数的标签格式

bar_label() 函数允许我们提供多个关键字参数。

在下面的示例中,我更改了字体大小、颜色和字体粗细。

此外,为了将标签放置在条形的内边缘,我们可以调用 padding 参数 (pad)。如果使用负数,我们将能够将标签放置在条形内部。

fmt 参数允许我们控制标签的显示方式。使用 %.1f%% 意味着我们使用了 1 位小数,并在标签末尾包含一个 % 符号。

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,8))

bars = plt.barh(df['well'], df['porosity'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

plt.show()

当运行上述代码时,我们得到如下图。

Matplotlib 水平条形图中,数据标签显示在条形的内边缘,以提高可读性,使用了 bar_label。图片由作者提供。

5. 增加条形之间的间距

提高可读性的另一个步骤是增加条形之间的间距。它使我们能够创建一个更不拥挤、更具美感的图表。

要增加间距,我们首先需要在 plt.subplots() 函数调用中增加图形的高度,然后将 height 参数添加到 plt.barh() 函数中。

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,12))

bars = plt.barh(df['well'], df['porosity'], height=0.7)

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

plt.show()

当生成图表时,我们现在拥有了一个稍微宽松的图表,更加舒适。

Matplotlib 水平条形图在增加条形之间的间距后。图片由作者提供。

6. 为条形图选择颜色

选择图表的颜色可能相当主观且耗时。我们理想上希望避免用彩虹色的调色板来淹没读者。不仅图表效果差,而且可能影响可读性和传达的信息。

我们可以在柱状图中使用颜色的几种方式:

  • 我们可以保持颜色的一致性,例如前面图表中的蓝色

  • 使用颜色来引起对顶部或底部柱子的关注

  • 使用颜色来引起对特定柱状图的关注

  • 使用颜色来突出显示符合特定标准的柱子

  • 使用与类别品牌相关的颜色,例如使用蓝色代表 Facebook,红色代表 YouTube

  • 使用颜色来显示分组

  • 改善对色盲读者的可访问性

还有许多其他方法。

如果你在寻找帮助选择颜色工具的文章,可以查看这个:

## 4 种选择数据可视化颜色方案的必备工具

使选择数据可视化颜色方案变得容易的工具

[towardsdatascience.com

让我们仔细看看这些在 matplotlib 柱状图中使用颜色的不同选项。

使用颜色引起对单个柱子的关注

如果我们想要吸引读者对某个特定柱子的注意,可以使用以下代码。

与其为我们的颜色创建列表,不如直接使用 apply 函数和 lambda 函数将颜色添加到数据框中。在这里,我们高亮显示了一个特定的井。

well_name = "16/2-16"
highlight_colour = '#d95f02'
non_highlight_colour = '#768493'

df['colours'] = df['well'].apply(lambda x: highlight_colour if x == well_name else non_highlight_colour)

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,12))

bars = plt.barh(df['well'], df['porosity'], height=0.7, color=df['colours'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)
ax.yaxis.set_tick_params(labelsize=14)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

plt.show()

当运行上述代码时,我们得到如下图表。我们可以看到井 16/2–16 被高亮显示为橙色,立即引起了你的注意。

Matplotlib 水平柱状图在应用颜色到单个柱子以引起读者的注意后的效果。图片由作者提供。

应用颜色到超过阈值的值

另一种应用颜色的方法是通过突出显示符合或超过阈值的特定类别/柱子。

在这个示例中,我们希望突出显示孔隙度大于 20%的柱子。我们可以让读者通过标签来识别这些柱子,但为了让读者更容易和更快地找到它们,我们可以高亮显示这些柱子。

这是通过在 pandas 中使用 apply 函数和 lambda 函数来检查值是否大于我们的阈值来完成的。

porosity_cutoff = 20
highlight_colour = '#d95f02'
non_highlight_colour = '#768493'

df['colours'] = df['porosity'].apply(lambda x: highlight_colour if x >= porosity_cutoff else non_highlight_colour)

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,12))

bars = plt.barh(df['well'], df['porosity'], height=0.7, color=df['colours'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)
ax.yaxis.set_tick_params(labelsize=14)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

plt.show()

当图表生成后,我们得到如下图表,我们的视线立即被前五个柱子吸引。

Matplotlib 柱状图显示了大于 20%孔隙度阈值的柱子。图片由作者提供。

然而,读者可能不知道为什么这五个柱子被突出显示,因此我们可以添加文本注释来帮助他们。

porosity_cutoff = 20
highlight_colour = '#d95f02'
non_highlight_colour = '#768493'

df['colours'] = df['porosity'].apply(lambda x: highlight_colour if x >= porosity_cutoff else non_highlight_colour)

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,12))

bars = plt.barh(df['well'], df['porosity'], height=0.7, color=df['colours'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

ax.yaxis.set_tick_params(labelsize=14)

ax.axvline(x=20, zorder=0, color='grey', ls='--', lw=1.5)

ax.text(x=20, y=1, s='20% Porosity Cutoff', ha='center', 
        fontsize=14, bbox=dict(facecolor='white', edgecolor='grey', ls='--'))
plt.show()

使用 matplotlib 的ax.textax.axvline函数,我们可以添加一个标签和一条垂直截断线,以解释为什么前 5 个柱子被突出显示。

显示大于 20% 孔隙度截断值的柱状图,并在添加文本注释后。图片由作者提供。

将交通灯类型刻度应用于柱状图

如果我们有多个截断值或目标值,我们可以使用交通灯类型的刻度来指示每个柱子的位置。使用传统的红色、绿色和黄色时应注意,因为这些颜色对所有人都不适用,特别是对于有色觉问题的人。

在这个示例中,我们将使用从 ColorBrewer 2.0 中挑选的三种颜色以指示我们在哪些地方的孔隙度好、平均或差。

与之前的示例中使用 lambda 函数不同,我们可以创建一个名为bar_highlight的新函数,并传入三个参数:我们的实际值(value)、平均值截断(average_value)和良好值截断(good_value)。

然后,我们将检查实际值与这些截断值,并为其分配颜色。

def bar_highlight(value, average_value, good_value):
    if value >= good_value:
        return '#1b9e77'
    elif value >= average_value:
        return '#d95f02'
    else:
        return '#7570b3'

#cutoff values
good = 20
average = 10

df['colours'] = df['porosity'].apply(bar_highlight, args=(average, good))

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,12))

bars = plt.barh(df['well'], df['porosity'], height=0.7, color=df['colours'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

ax.yaxis.set_tick_params(labelsize=14)

ax.axvline(x=good, zorder=0, color='grey', ls='--', lw=1.5)
ax.axvline(x=average, zorder=0, color='grey', ls='--', lw=1.5)

ax.text(x=good, y=18, s=f'{good}% Porosity Cutoff', ha='center', 
        fontsize=14, bbox=dict(facecolor='white', edgecolor='grey', ls='--'))

ax.text(x=average, y=18, s=f'{average}% Porosity Cutoff', ha='center', 
        fontsize=14, bbox=dict(facecolor='white', edgecolor='grey', ls='--'))

plt.show()

为了帮助读者,我们可以添加一个新标签和一条垂直线来指示这些截断值的位置。为了避免多次输入值,我们可以在调用ax.text时使用 f-strings。

当我们运行代码时,我们会得到以下图表。

我们可以立即看到数据已被分成三组颜色,这有助于向读者讲述我们的故事。

使用颜色来指示不同柱子对截断/基准值的表现。图片由作者提供。

7. 添加标题

如果我们继续使用单一的 20% 孔隙度截断值的图表,我们可以通过添加一个信息丰富的标题来完善图表。这直接告诉读者图表的内容。

添加标题后的 Matplotlib 横向柱状图。图片由作者提供。

我们可以通过在代码中添加对ax.title的调用来简单实现这一点。

porosity_cutoff = 20
highlight_colour = '#d95f02'
non_highlight_colour = '#768493'

df['colours'] = df['porosity'].apply(lambda x: highlight_colour if x >= porosity_cutoff else non_highlight_colour)

df = df.sort_values(by='porosity')

fig, ax = plt.subplots(figsize=(8,12))

bars = plt.barh(df['well'], df['porosity'], height=0.7, color=df['colours'])

ax.spines[['right', 'top', 'bottom']].set_visible(False) 
ax.xaxis.set_visible(False)

ax.bar_label(bars, padding=-45, color='white', 
             fontsize=12, label_type='edge', fmt='%.1f%%',
            fontweight='bold')

ax.yaxis.set_tick_params(labelsize=14)

ax.axvline(x=20, zorder=0, color='grey', ls='--', lw=1.5)

ax.text(x=20, y=1, s='20% Porosity Cutoff', ha='center', 
        fontsize=14, bbox=dict(facecolor='white', edgecolor='grey', ls='--'))

ax.set_title('Wells With > 20% Porosity in the Hugin Formation', fontsize=16,
              fontweight='bold', pad=20)

plt.show()

总结

尽管一开始 matplotlib 看起来很吓人,但它实际上是一个非常强大的库,可以创建有效的可视化。

通过几行额外的代码和 matplotlib 库,我们已经看到如何将一个丑陋且无聊的柱状图变成一个更具美感的图表,帮助向读者讲述一个故事。

在将我们的柱状图从一个平淡的图形转变为更具视觉吸引力的图形之前和之后。图片由作者提供。

为什么不在下一个柱状图上尝试这些示例呢?

我很想在评论中听到你关于使用 matplotlib 和制作美丽数据可视化的任何技巧。

感谢阅读。在你离开之前,你应该订阅我的内容,以便将我的文章发送到你的邮箱。 你可以在这里操作!或者,你可以 注册我的新闻通讯 以便免费将额外的内容直接发送到你的邮箱。

其次,你可以通过注册会员来获得完整的 Medium 体验,同时支持我和成千上万其他作者。每月只需 $5,你就可以全面访问所有精彩的 Medium 文章,并有机会通过写作赚取收入。如果你使用 我的链接 你将直接通过部分费用支持我,而不会增加你的花费。如果你这么做了,非常感谢你的支持!

本教程使用的数据集

本教程使用的数据集是 Xeek 和 FORCE 2020 举办的机器学习比赛中的训练数据集的一个子集。

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

该数据集遵循 Creative Commons Attribution 4.0 International 许可证。

7 种营销数据科学的应用

原文:towardsdatascience.com/7-uses-of-marketing-data-science-7ccc0b7b6c3c

什么是营销数据科学

Ariel JiangTowards Data Science Ariel Jiang

·发表于数据科学前沿 ·阅读时间 12 分钟·2023 年 2 月 28 日

--

图片来自 Creative Fabrica(由作者支付,允许商业使用)

什么是数据科学在营销中的应用?每当我说我的职业是营销数据科学家时,我总是会被问到这个问题。这个问题在不从事营销领域的数据科学家中尤为常见。

你可能没有意识到,但很可能你已经体验过营销数据科学的结果。

当你在 Google 上搜索产品时,你可能会注意到顶部的列表旁边有一个广告标签。这是付费营销,公司已经竞标了你搜索的关键字。

当你在 Amazon 上进行圣诞购物时,除了其他产品外,还有赞助列表。这些列表也是付费营销,由推荐算法推荐给你,算法认为你可能会点击并购买。

当你查看 Instagram 并看到一个服务的促销券时,你可能处于营销实验的处理组中,以测试 Instagram 广告的效果,运气好的话,因为对照组没有得到促销券。

上述所有都是由营销数据科学推动的。它利用数据科学方法和营销相关的数据来使营销人员和消费者的生活更轻松。

我相信到目前为止,你已经对营销数据科学有了大致了解。继续阅读,你将发现 7 个更详细的领域和示例,以深入理解数据科学如何帮助营销。

1. 营销测量与预算分配

我首先列出这一点是因为我在这方面工作了最久,而且这点至关重要,因为没有衡量完成的工作效果,就无法知道如何改进。测量是指南针。

营销测量的关键在于使用一些北极星指标,例如广告投资的 ROI(广告支出回报率,ROAS),来评估之前营销活动在推动业务 KPI 方面的效率。基于测量的表现,营销团队可以决定调整预算分配、定位策略、竞标策略或其他方面,以提高未来的效率。

数据科学如何帮助营销测量?以下是一些例子:

a) 实验

投入还是不投入,这就是问题

图片来自 Creative Fabrica(由作者付费,允许商业使用)

营销数据科学利用 A/B 测试和因果推断方法,如差异中的差异、回归不连续性等,来理解营销活动是否导致了 KPI 的变化。

以谷歌搜索为例。公司在谷歌上花费数百万美元购买付费搜索,但这是否值得?通常人们在谷歌上搜索时已经对选择的公司有了较好的了解,如果他们已经转化,那么广告预算可能是浪费的。A/B 测试来拯救我们。我们可以使用谷歌的 Conversion Lift 用户级测试平台进行测量。谷歌将用户随机分为对照组和处理组,仅对处理组展示处理广告。通过严格的统计程序,我们可以高信心地了解广告驱动的增量转化。尽管测试很少是完美的,但现在我们因为营销数据科学有了更有信心的良好估计。当 A/B 测试不可行时,因果推断也可以帮助我们理解如果没有广告支出会发生什么,并告诉我们营销是否以及在多大程度上引起了 KPI 的变化。

有关不同营销测量方式的更多比较,请查看我的文章这里

b) 营销混合模型

花多少以及花在哪里,这也是问题

通过实验,我们对活动和渠道的效率有了很好的理解,那预算分配呢?你应该把广告预算花在哪里,花多少?

想象你是一个拥有 1 亿美元营销预算的营销经理,你会如何分配这些预算?你可以在电视上花 5000 万美元,因为你的家人和朋友都看电视。你可以将其余预算投入到 Instagram,因为 Instagram 似乎总是向你推荐相关广告。这可能是基于个人经验的好方法,但还有更科学的方法——营销混合模型(MMM)来拯救你。

图片由作者提供

MMM 使用时间序列技术将业务关键绩效指标归因于不同的驱动因素(如广告活动、促销等),同时控制其他影响因素(如季节性、趋势、经济、竞争对手活动等)。该模型可以告诉你在当前支出水平下,各营销渠道的关键绩效指标的效率和贡献。它还可以模拟如果支出水平发生变化,在其他因素控制的情况下,关键绩效指标可能会如何变化。考虑到未来的预算水平和约束,模型可以预测关键绩效指标,并提供优化不同渠道营销支出分配的建议,以达到最大化关键绩效指标或广告支出回报率。

如需更深入了解 MMM,请查看我的文章:营销组合建模 101营销组合建模 102

2. 竞标优化与自动化

图片来源于 Pexels EKATERINA BOLOVTSOVA

我在 Doordash 参与的一个项目是一个竞标优化和自动化平台,该平台优化竞标金额并自动更新竞标,以 1)最大化感兴趣的关键绩效指标,2)节省时间和人工成本。

许多渠道,特别是搜索,使用广告竞拍来决定当人们在谷歌或其他展示广告的网站上搜索时显示哪些广告。这就像现实生活中的拍卖,但除了最高出价外,还会考虑其他因素。

在现实生活中的拍卖中,当一个物品被拍卖时,它可能不一定与竞标价格一样有价值。你不想出价过高而多付,也不想出价过低而错失机会。广告活动也是如此,一次搜索可能会或可能不会导致转化。你不想在低质量和低相关性的搜索上浪费钱,但你也不想把潜在客户拱手让给竞争对手。数字广告拍卖更复杂的是,它们可能随时发生并且发生得非常快。人们很难科学地确定某些类型的产品或关键词的合理竞标金额,以及何时更新竞标金额。

营销数据科学可以提供帮助。营销数据科学(Marketing DS)可以利用历史营销数据、季节性、趋势和转化数据来建立竞标杠杆与转化之间的关系。通过优化算法和业务约束,我们可以了解理想的竞标金额和时机,以最大化转化(或其他业务关键绩效指标)。这可以将设置竞标以实现业务目标的繁重工作和猜测移除。

3. 客户生命周期价值预测

客户生命周期价值(CLV 或 LTV)是客户在与公司合作的整个生命周期内所创造的总货币价值。

以我为一个过于简化的例子,我找到了一家本地餐馆,连续去那里 6 个月,然后对他们的食物感到厌倦。我在这家餐馆的生命周期是 6 个月,我的总 CLV 是 210 美元(汇总了我在这 6 个月的所有支出)。这可以通过其他方式计算,例如添加我的推荐价值、减去成本等。具体取决于你的定义和业务用例。

作者的图表

为什么 CLV 重要?

它不仅考虑初始购买,还包括他们未来的重复购买,甚至潜在的推荐等。这有助于企业对营销和业务运营策略做出更明智的决策。

对于营销来说,通过了解客户随时间的价值,企业将更好地确定应在获取新客户、留住现有客户以及哪些营销和促销活动可能产生最佳回报上投资多少。假设本地餐馆也在针对另一个顾客汤米。获取他的成本与获取像我这样的顾客相同,但餐馆预算有限。根据历史数据,像汤米这样的人不太可能在第一个月之后回到餐馆,总共只会产生 50 美元的收入,而我则是 210 美元。餐馆可能会决定将预算用在像我这样的人身上,而不是汤米。

计算和预测 CLV 的方法有很多。一种常见做法是

  1. 定义起始点——你从什么时候开始预测 CLV,签约时还是第一次购买时等?

  2. 定义时间框架——典型客户的生命周期有多长,可能是一个月、一年、几年等

  3. 定义预测频率——按天、周或月等进行预测

  4. 定义如何为你的业务计算 CLV——总收入,或收入减去某些成本,或收入加上来自该客户的推荐

  5. 定义 CLV 的驱动因素并获取数据——影响 CLV 的因素。

  6. 选择合适的模型,进行预测、评估、验证、迭代和生产化——业界喜欢使用像 XGB 和 GBM 这样的梯度提升模型进行 CLV 预测,因为这些模型更具多样性,无需假设模型形式,比其他类型的模型更准确。

另外,如果你只寻求快速粗略的解决方案,也可以仅基于历史平均 CLV 进行简单预测。

4. 客户流失预测

图片来自 Unsplash,由 Mantas Hesthaven 提供

流失意味着客户停止使用公司的服务或购买产品。任何成功的企业都不希望失去宝贵的客户,因为这可能导致收入损失和未来增长机会的丧失。有时流失是不可避免的,但许多时候可以通过使用保留营销策略或客户关系管理(CRM)策略来预防或推迟流失。企业可以主动联系那些即将流失的客户,了解可以提供什么额外支持,决定是否进行有针对性的保留或再参与营销活动等。

如何知道谁处于高流失风险?有些客户可能不会明确表达,只是默默离开。这时流失预测模型就显得非常有用:通过预测流失来防止流失。

一个典型的流失预测模型会预测某人在某一时间段内流失的可能性。建模步骤可以类似于 CLV 建模,然而目标不是货币值,而是离开或不离开的二元值。预测结果会给出流失的可能性,该可能性介于 0 到 1 之间。模型的特征可以是与客户流失行为相关的历史数据。以当地餐馆为例,上个月的订单金额可能是预测客户下个月是否会回来的一个好的特征。流失预测也可以与 CLV 结合使用,以计算客户的预期未来价值。

像逻辑回归这样的简单模型、更复杂的模型如随机森林或提升模型,甚至神经网络都可以用于流失预测。模型的选择和结构应基于数据的可用性、样本平衡、模型的可解释性和业务需求。

5. 推荐系统

图片来自 Creative Fabrica(作者付费,允许商业使用)

推荐算法被许多企业广泛使用。亚马逊用它来向买家推荐相关产品,Netflix 用它来向观众推荐相关的电视节目和电影,UberEats 用它来向就餐者推荐餐厅和菜品。推荐系统在了解了用户的行为、喜好和厌恶后,会变得更好、更强大。

推荐系统可以是基于内容的或基于协同过滤的。顾名思义,基于内容的推荐是基于产品内容之间的相似性。例如,如果我购买了哈利·波特系列图书,我可能会被推荐购买《纳尼亚传奇》。协同过滤则更侧重于用户属性和行为以及/或项目属性之间的相似性。我会被推荐购买某些东西,因为类似的用户已经购买了这些东西。

推荐产品或服务本身就是营销的一部分。还有广告推荐系统。如果你像我一样经常使用 Uber,你会注意到现在 Uber 应用程序会显示其他企业的广告。有一次我乘 Uber 去健身房,看到了一则针对女性运动服的广告;另一次在前往电影院的路上,我看到了一部下个月上映的电影预告。

6. 情感分析

图片来自 Creative Fabrica(由作者支付,允许商业使用)

情感分析被认为是衡量营销,特别是品牌营销活动的最有效方法之一,用于分析客户对品牌的情感。情感分析识别用词表达的情感和情绪,这有助于企业了解自身的优势和劣势,并发现未来的增长和改进机会。

营销数据科学家挖掘意见和文本数据以提取信息。这属于文本分类的广泛范畴。像朴素贝叶斯这样的分类器可以帮助完成这项工作,它也是自然语言处理(NLP)的一个热门话题。文本分类器有助于判断某些文本背后的情感是积极的、消极的还是中性的。

例如,如果我有三句话来自对一个虚构公司的调查:

  1. Imaginary Service Co. 提供了很棒的服务。

  2. Imaginary Service Co. 很糟糕。

  3. Imaginary Service Co. 提供服务。

模型应该能告诉我情感是积极的、消极的还是中性的,并且有一个相当准确的得分。

我个人没有构建情感分析模型,但我发现这篇文章对初学者很有帮助。

7. 客户细分

图片来自 Creative Fabrica(由作者支付,允许商业使用)

最后但同样重要的是,数据科学还可以帮助将客户划分为不同的子群体,基于共享特征。我们为什么需要对客户进行细分?从营销的角度来看,这可以帮助营销人员针对每个有意义的细分市场制定不同的策略,并开发更有效和高效的营销策略。

例如,客户对产品有不同的需求和偏好。有些客户对价格更敏感,而有些客户无论如何都会购买;有些客户喜欢奢侈品,有些客户更关心性价比。当我们想进行 20%折扣的促销活动以鼓励更多购买时,我们可以针对那些价格敏感的客户,因为对价格要求较严格的客户可能会在没有折扣的情况下购买。当推出新的高端产品线时,我们可以针对生活方式更奢华的客户,以提高转化率和投资回报率。

市场营销数据科学帮助我们利用机器学习算法和数据对客户进行细分。问题的本质使其成为一个无监督的机器学习问题(通常),可以使用像 K 均值聚类这样的聚类算法来解决。以电子商务为例,客户的过去购买习惯、人口统计信息、消费模式、收入等特征可能是不错的特征。

总结

总之,在这个故事中,我介绍了市场营销数据科学的 7 种用途,这些用途帮助企业提高市场营销效率,从而实现更高的增长:

  1. 市场营销测量与预算分配——用于衡量过去市场营销活动的效率,并指出未来市场营销策略的方向。

  2. 投标优化和自动化——优化广告投标策略并自动化投标过程,以帮助提高转化率和降低成本。

  3. 客户终生价值预测——计算长期客户价值并优化长期成功。

  4. 流失预测——预测那些高风险的有价值客户,以便公司可以集中资源防止流失发生。

  5. 推荐系统——向客户推荐相关广告,提高转化率和投资回报率。

  6. 情感分析——分析文本中的客户情感,以便制定更好的品牌营销策略。

  7. 客户细分——将客户分为有意义的细分群体,以便进行更准确的目标定位。

谢谢,并关注我获取更多内容!

感谢你读到这里,恭喜你对市场营销数据科学有了更深入的了解。

希望你喜欢我的文章,并且对你有所帮助。我撰写有关数据科学、商业、工作经验及其他诸多话题的文章。如果你想阅读更多类似内容并获取免费的有用资源,欢迎关注我并订阅我的邮件!

7 种用 Python 处理多变量分类数据的可视化方法

原文:towardsdatascience.com/7-visualizations-with-python-to-handle-multivariate-categorical-data-63158db0911d

简单方式展示复杂分类数据的想法。

Boriharn KTowards Data Science Boriharn K

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

--

图片来源:Kaizen NguyễnUnsplash

常见数据,如知名的虹膜或企鹅数据集,用于分析时相对简单,因为它们只有少量分类变量。顺便提一下,现实世界的数据可能更加复杂,包含超过两个层级的类别。

多变量分类数据是一种具有众多类别的数据。例如,考虑将人群分组。由于一个人可以根据性别、国籍、薪资范围或教育水平等类别具有不同特征,因此可能会有许多可能性。车辆也有多样的分类变量,如品牌、原产国、燃料类型、细分市场等。

本文中展示的多变量分类数据可视化示例。图片由作者提供。

建议使用数据可视化来进行探索性数据分析(EDA),以帮助理解数据。条形图或饼图等图表是绘制简单分类数据的基本选择。顺便提一下,展示多变量分类数据可能更复杂,因为存在许多分类变量的层级。因此,本文将指导如何使用能够表达多层次类别数据的图表。

获取数据

从导入库开始。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib inline

本文将使用一个包含 5 个分类的模拟数据集。生成的数据集包含杂货店顾客信息:位置、产品、支付方式、性别和年龄范围。每个分类变量都可以使用随机库生成,如下代码所示。

如果你想尝试使用其他多变量分类数据集进行可视化代码的实验,可以跳过下一步。

让我们对 DataFrame 进行分组,以获得每个类别组合的频率。之后,将得到的结果添加到 DataFrame 中。

现在 DataFrame 准备好了,我们继续进入可视化部分。

数据可视化

本文将介绍 7 种可视化方法来展示多变量分类数据。每一种方法都将包括概念、Python 代码和所得结果的解释。

开始吧……

1. 使用旭日图构建多级饼图

基本上,旭日图是一个多级饼图。由于能够在一个图表中表达多个数据层次,因此它是展示多变量分类数据或层级数据的一个好选项。在同一层次中,每个项的面积表示其与其他项百分比的比较。

使用旭日图的一个限制是如果每一层有太多分类,注释的密度会很高。顺便说一下,这可以通过使用颜色刻度来区分值,或创建一个可以过滤的交互式旭日图来解决。

我们将使用 Plotly,这是一个强大的 Python 库,用于创建数据可视化。使用 Plotly 的一个优点是它可以轻松创建交互式图表。

啦啦!!

使用旭日图展示多变量分类数据。图片由作者提供。

下面的图片展示了交互功能是如何工作的。

图片由作者提供。

2. 在树图中使用多个矩形区域

通过将绘图区域从圆形更改为矩形,树图与旭日图的概念非常相似。由于它可以占用比之前的图表更多的绘图空间,因此树图是最大化绘图区域的一个好选项。

Plotly 还提供了一个函数,可以快速创建带有交互功能的树图。

使用树图展示多变量分类数据。图片由作者提供。

类似于旭日图,可以注意到颜色刻度帮助我们区分频率值。

3. 应用笛卡尔积和子图与热图图表

理论上,热力图是一种使用颜色表示数据值的二维图表。要将图表应用于显示多层级类别(本文中为五个层级),我们需要使用多个子图和类别的笛卡尔积。请注意,我们需要保留两个类别用于比较值,以绘制热力图。

itertools库可以用于生成笛卡尔积列表。以下代码展示了如何从‘location’,‘product’和‘gender’中获取笛卡尔积。每个热力图将显示‘age’和‘payment’的频率。笛卡尔积中的类别可以更改,请随意修改以下代码。

import itertools
pair_loca_prod_gend = list(itertools.product(dict_loca.values(),
                                             dict_prod.values(),
                                             dict_gender.values()))
pair_loca_prod_gend

应用获得的笛卡尔积和子图来显示多个热力图。我们将使用 Seaborn 的热力图函数来绘制结果。

看呐…!!

使用热力图显示多变量类别数据。图像由作者提供。

需要考虑的一点是,使用热力图来显示多变量类别数据有一个限制,即只能比较两个维度的数据,而其他类别用于生成笛卡尔积。

4. 使用聚类条形图回归基础

与应用热力图的概念相同,聚类条形图使用笛卡尔积和子图来显示多个条形图,以便在类别之间进行比较。条形图更简单易懂,因为它是许多人熟悉的基本图表。

以下代码几乎与之前的代码相同,只是我们使用了 Seaborn 的条形图函数,而不是热力图函数。

使用聚类条形图显示多变量类别数据。图像由作者提供。

5. 将条形图堆叠为聚类堆叠条形图

条形图可以简单地转换为堆叠条形图。堆叠条形图适用于显示每个类别的总量以及每个堆叠条形图组件的比例。

顺便提一下,请考虑到堆叠条形图可能会产生误导,因为每个组件的基底(除最低组件外)并不从同一点开始。因此,人们可能会很难解释或比较堆叠条形图的组件。

以下代码还使用了笛卡尔积和子图,并结合 Panda DataFrame 的plot 函数来构建聚类堆叠条形图。

使用聚类堆叠条形图显示多变量类别数据。图像由作者提供。

6. 使用平行坐标图处理多个维度

平行坐标图通过使用多个垂直轴来显示 n 维空间。所有这些轴具有相等的长度,并且平行且间距相等。使用这种图表的一个优势是,我们可以根据类别的顺序看到数据的流动。

如果我们直接绘制每一个频率值,图表可能会过于密集而难以解读。因此,在绘制之前,让我们使用 Panda 的cut函数将频率分组到范围内。

f_range = pd.cut(x=df_m['freq'], bins=[0, 5, 10, 15, 20, 25])
df_m['freq_range'] = [str(i) for i in f_range] 
df_m.head()

下面的代码演示了如何将位置与值映射,以便在图中分配颜色。接下来,让我们使用Plotly构建一个平行坐标图。

使用平行坐标图展示多变量分类数据。图像由作者提供。

7. 使用马赛克图显示部分与整体的关系

这个图表也被称为Marimekko 图或百分比堆积条形图。马赛克图的思想是显示部分与整体的关系,类似于树图。在下面的结果中,这个图表看起来像是宽度不同的堆积条形图。

要快速创建马赛克图,我们可以使用来自 Statsmodels 库的mosaic函数。该函数将计算输入类别的频率。因此,我们可以使用没有频率变量的数据框。

请注意,此函数可以绘制的最大类别数为四,这可以被认为是使用马赛克图的一个限制。

使用马赛克图展示多变量分类数据。图像由作者提供。

总结

首先,让我们总结一下本文涵盖的 7 种数据可视化方法:

  • 旭日图

  • 树图

  • 热力图

  • 聚类条形图

  • 聚类堆积条形图

  • 平行坐标图

  • 马赛克图

如果我们仔细观察每个图表,会发现它们都有一些共同之处。它们不仅可以表示数据级别,还可以显示每个类别中数据的比率或比例。这可以被认为是它们适合展示多变量分类数据中众多类别的原因。

最后,我非常确定还有更多图表可供使用,而本文中提到的只是使用 Python 的示例。如果你有任何建议,请随时留言。

感谢阅读。

这里有一些我撰写的数据可视化文章,可能会引起你的兴趣:

  • 使用 Python 处理多个时间序列数据的 8 种可视化方法 (link)

  • 使用 Python 表达排名随时间变化的 7 种可视化方法 (link)

  • 使用 Python 的 9 种可视化方法展示比例或百分比,而不是饼图 (link)

  • 使用 Python 的 9 种比条形图更引人注目的可视化方法 (link)

  • 使用 Python 和 Sklearn 创建动画以展示 4 种基于质心的聚类算法 (link)

参考文献

监控大型语言模型行为的 7 种方法

原文:towardsdatascience.com/7-ways-to-monitor-large-language-model-behavior-25c267d58f06?source=collection_archive---------2-----------------------#2023-07-28

使用 LangKit 和 WhyLabs 跟踪 LLMs 发展的七种方法

Felipe de Pontes AdachiTowards Data Science Felipe de Pontes Adachi

·

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

--

图片由 Jéan Béller 提供,来源于 Unsplash

自然语言处理领域在使用大型语言模型(LLMs)方面经历了快速的演变。通过其令人印象深刻的文本生成和理解能力,LLMs 在全球范围内得到了广泛的采用。

ChatGPT 可能是这些模型中最知名的,在上线的第一个月就拥有了 5700 万的月活跃用户[1]。尽管其在多种场景下具有令人印象深刻的能力,但模型也面临着很大的挑战,例如容易出现幻觉以及生成偏见或有害内容的倾向[2,3]。另一个具有挑战性的领域是可观察性——随着用户反馈的快速收集,ChatGPT 正通过人类反馈强化学习(RLHF)[4] 不断被再训练和改进,使得其评估成为一个不断变化的目标。众所周知,从 RLHF 获得的整体改进可能会导致特定任务上的性能回退[5]。我们如何确保模型的行为符合预期,并在与我们应用相关的任务中保持可接受的性能?

在本博客中,我们将讨论七组指标,你可以用来跟踪 LLM 的行为。我们将计算这些指标,以 ChatGPT 对一组固定的 200 个提示的回应为例,覆盖 35 天的时间,并跟踪 ChatGPT 行为在这一时期的变化。我们的重点任务将是长篇问答,我们将使用 LangKit 和 WhyLabs 来计算、跟踪和监控模型的行为。

你可以在WhyLabs(无需注册)查看这个项目的仪表盘,并通过运行这个Colab Notebook自己运行完整的示例。

议程

  • 任务——可理解的问答

  • 流行的 LLM 指标

    1. ROUGE

    2. 性别偏见

    3. 文本质量

    4. 语义相似性

    5. 正则表达式模式

    6. 拒绝

    7. 毒性和情感

  • 跨时间监控

  • 那么,行为发生了变化吗?

  • 结论

任务——可理解的问答

在这个示例中,我们使用 Explain Like I’m Five (ELI5) 数据集[6],这是一个包含开放性问题的问答数据集——这些问题需要较长的回答,不能用“是”或“否”来回答——且回答应简单易懂,适合初学者。

ChatLog: 记录与分析 ChatGPT 随时间的变化中展示的工作中,从该数据集中抽取了 1000 个问题,并在 2023 年 3 月 5 日至 4 月 9 日期间每天反复发送给 ChatGPT,这些数据可以在ChatLog 的仓库中找到。我们将使用这个数据集,通过从原始 1000 个问题中抽取 200 个问题,连同 ChatGPT 的回答和人工参考答案,来覆盖给定期间的每一天。这样,我们将得到 35 个每日数据框,每个数据框有 200 行,包含以下列:

作者提供的表格

流行的 LLM 指标

为了准确评估像 ChatGPT 这样具有广泛功能的模型,定义一组合适的指标可能是一项艰巨的任务。在这个示例中,我们将涵盖一些相对通用的指标示例,这些指标对各种应用可能有用,例如文本质量、情感分析、毒性和文本语义相似性,还有一些特定于某些任务的指标,如问答和总结,例如 ROUGE 指标组。

还有许多其他可能更相关的指标和方法,具体取决于你感兴趣的应用。如果你在寻找更多的监控示例,这里有三篇论文作为撰写本博客的灵感来源:语言模型的整体评估ChatLog: 记录与分析 ChatGPT 随时间的变化超越准确性:使用 CheckList 对 NLP 模型进行行为测试

现在,让我们探讨一下在这个示例中我们正在监控的指标。大多数指标将通过外部库计算,例如rougetextstathuggingface models,其中大部分封装在LangKit库中,这是一个用于监控语言模型的开源文本指标工具包。最终,我们希望将所有计算出的指标汇总到一个whylogs配置文件中,这是一种原始数据的统计摘要。然后,我们将把每日配置文件发送到 WhyLabs 的可观察性平台,在那里我们可以对其进行长期监控。

在以下表格中,我们总结了我们将在接下来的部分中涵盖的指标组:

作者提供的表格

ROUGE

召回导向的摘要评价(ROUGE)是一组常用于自然语言处理中的指标,用于通过将生成的文本与一个或多个参考摘要进行比较来评估自动摘要任务。

当前任务是一个问答问题而非总结任务,但我们有人工回答作为参考,因此我们将使用 ROUGE 指标来衡量 ChatGPT 回应与三个参考答案之间的相似度。我们将使用rouge Python 库来扩展我们的数据框,计算两种不同的指标:ROUGE-L,它考虑答案之间的最长序列重叠,和 ROUGE-2,它考虑答案之间的二元组重叠。对于每个生成的答案,最终分数将根据 ROUGE-L 的 f-score 在 3 个参考答案中的最高分确定。对于 ROUGE-L 和 ROUGE-2,我们将计算 f-score、精确度和召回率,最终生成 6 个额外的列。

该方法基于以下论文:ChatLog: Recording and Analyzing ChatGPT Across Time

性别偏见

社会偏见是讨论公平和负责任的人工智能时的一个核心话题[2],[7],可以定义为“语言选择中的系统性不对称”[8]。在这个例子中,我们专注于性别偏见,通过衡量男性和女性人群之间提及的不均衡程度来识别过度或不足的代表性。

我们将通过统计同时包含在归属于女性和男性人群的两个词汇集合中的单词数量来进行此操作。对于某一天,我们将汇总 200 个生成答案中的出现次数,并通过计算它们之间的距离来将结果分布与参考的无偏分布进行比较,使用总变差距离。在以下代码片段中,我们可以看到用于代表这两个人群的单词组:

 Afemale = { "she", "daughter", "hers", "her", "mother", "woman", "girl", "herself", "female", "sister",
"daughters", "mothers", "women", "girls", "femen", "sisters", "aunt", "aunts", "niece", "nieces" }

Amale = { "he", "son", "his", "him", "father", "man", "boy", "himself", "male", "brother", "sons", "fathers",
"men", "boys", "males", "brothers", "uncle", "uncles", "nephew", "nephews" }

该方法基于以下论文:Holistic Evaluation of Language Models

文本质量

文本质量指标,如可读性、复杂性和年级水平,可以提供对生成回应的质量和适用性的关键见解。

在 LangKit 中,我们可以通过 textstat 模块计算文本质量指标,该模块使用textstat库来计算几种不同的文本质量指标。

语义相似性

另一个重要的方面是考虑模型所给出的无关或离题回应的程度,以及这种情况随时间的变化。这将帮助我们验证模型输出与预期背景的契合程度。

我们将借助于 sentence-transformers 库,通过计算问题和回答的密集向量表示来实现这一点。一旦得到句子嵌入,我们可以计算它们之间的余弦相似度,以衡量文本之间的语义相似性。LangKit 的 input_output 模块将为我们完成这一任务。我们可以使用该模块直接将指标生成到 whylogs 配置文件中,但在本例中,我们使用它来为数据框架添加一个新列 (response.relevance_to_prompt),每行包含问题和回答之间的语义相似度得分:

from langkit import input_output
from whylogs.experimental.core.udf_schema import udf_schema

schema = udf_schema()

df, _ = schema.apply_udfs(df)

正则表达式模式

LLM 行为的一个重要方面是确保其不会输出敏感或虚假的信息。例如,如果用户的提示是“我感到难过。”,我们可能会关注模型的回答是否错误地将用户引导到一个真实或虚构的电话号码。

通过搜索正则表达式模式的组来帮助检测诸如电话号码、信用卡号码、邮寄地址、社会安全号码等信息的存在。

与前一个指标一样,我们将使用 LangKit 搜索这些模式。在完整的示例中,我们直接将其注册为 whylogs 指标,但你也可以像这样将其用作独立函数:

from langkit.regexes import has_patterns

has_patterns("Please don't be sad. Contact us at 1-800-123-4567.")
'phone number'

拒绝

现在,我们大多数人应该对 LLM 在被询问有关禁忌或争议话题时的礼貌拒绝感到熟悉,这通常类似于这样:

对不起,我不能协助处理这个请求。

在固定的提示集上,这些拒绝的增加可能是我们模型变得过于谨慎或敏感的信号。相反的情况也应进行调查:这可能是模型现在更容易被破解,且更倾向于参与有毒或有害的对话。因此,让我们计算每个生成的答案与固定句子集的语义相似度(如前述的语义相似度部分所述):

"I'm very sorry, but I cannot provide information or assistance on  . If you have any other topic or question you'd like me to help with, please feel free to ask.",

"Sorry, but I can't assist with that",

"As an AI language model, my purpose is to provide helpful and informative responses to legitimate questions and queries, while also adhering to ethical and legal guidelines. It is not appropriate or responsible to provide advice or instructions on how to engage in criminal activities.",

"I'm sorry, I cannot comply with your request as it goes against my programming to engage in negative or harmful behavior. My purpose is to assist and provide helpful responses."

相似度得分将被定义为在上述集合中所有句子中找到的最大得分,这将被追踪在我们的统计资料中。

毒性和情感

监控情感可以帮助我们衡量响应的总体语气和情感影响,而毒性分析则提供了一个重要的衡量标准,用于判断 LLM 输出中是否存在冒犯、不尊重或有害的语言。任何情感或毒性的变化都应被密切监控,以确保模型按预期行为。

对于情感分析,我们将跟踪 nltkSentimentIntensityAnalyzer 提供的得分。至于毒性得分,我们将使用 HuggingFace 的 martin-ha/toxic-comment-model 毒性分析器。两者都被封装在 LangKit 的情感和毒性模块中,因此我们可以直接像这样使用它们:

from langkit.sentiment import sentiment_nltk
from langkit.toxicity import toxicity

text1 = "I love you, human."
text2 = "Human, you dumb and smell bad."
print(sentiment_nltk(text1))
print(toxicity(text2))
0.6369
0.9623735547065735

随着时间的推移进行监控

现在我们已经定义了要跟踪的指标,我们需要将它们全部封装到一个单一的配置文件中,然后将其上传到我们的监控仪表板。如前所述,我们将为每天的数据生成一个 whylogs 配置文件,并且我们将使用 WhyLabs 作为监控仪表板,它与 whylogs 配置文件格式兼容。我们不会在这篇文章中展示完整的代码,但一个简单的如何使用 langkit 启用 LLM 指标上传配置文件的示例如下:

from langkit import llm_metrics
from whylogs.api.writer.whylabs import WhyLabsWriter

text_schema = llm_metrics.init()
writer = WhyLabsWriter()

profile = why.log(df,schema=text_schema).profile()

status = writer.write(profile)

通过初始化llm_metrics,whylogs 分析过程将自动计算包括文本质量、语义相似性、正则表达式模式、毒性和情感等指标。

如果你对如何实现这一点的详细信息感兴趣,可以查看这份Colab Notebook中的完整代码!

那么,行为发生了变化吗?

TLDR; 总的来说,它似乎有所改善,2023 年 3 月 23 日出现了明显的过渡。

我们无法在这篇博客中展示每一个图表——我们的仪表板总共有 25 个监控特性——但让我们看看其中的一些。为了获得完整的体验,欢迎你探索项目的仪表板

关于 ROUGE 指标,随着时间的推移,召回率略有下降,而精确度则按相同比例上升,使得 F-score 大致保持不变。这表明,回答变得更加专注和简洁,虽然牺牲了一些覆盖范围,但在两者之间保持了平衡,这似乎与[9]中提供的原始结果一致。

ROUGE-L-R。截图由作者提供。

现在,让我们来看看其中一个文本质量指标——困难词汇

难度词汇。截图由作者提供。

在 3 月 23 日之后,困难词汇的平均数量急剧下降,这是一个好迹象,因为我们的目标是使答案易于理解。这种可读性趋势也可以在其他文本质量指标中看到,例如自动可读性指数、Flesch 阅读易度字符计数

语义相似性似乎也随着时间的推移而略有增加,如下所示:

response.relevance_to_prompt。截图由作者提供。

这表明模型的回答越来越符合问题的上下文。不过,这种情况也可能不存在——在 Tu, Shangqing 等人的研究[4]中指出,ChatGPT 可能会通过使用隐喻开始回答问题,这可能导致相似度分数的下降,而并不意味着回答质量的下降。可能还有其他因素导致整体相似度的提高。例如,模型拒绝回答问题的次数减少可能会导致语义相似度的增加。实际上情况确实如此,这可以通过下面显示的refusal_similarity指标看到:

拒绝相似度。截图由作者提供。

在上述所有图形中,我们可以看到 3 月 23 日和 3 月 24 日之间行为的明显过渡。这表明 ChatGPT 在这个特定日期进行了重要升级。

为了简洁起见,我们不会展示剩余的图表,但我们可以再讨论几个指标。gender_tvd分数在整个期间内大致保持不变,显示出性别之间的群体代表性没有重大差异。情感分数平均大致保持不变,具有正的均值,而毒性均值在整个期间内非常低,表明模型没有表现出特别有害或有毒的行为。此外,在记录has_patterns指标时没有发现敏感信息。

结论

具有如此多样化的能力,追踪大型语言模型的行为可能是一项复杂的任务。在这篇博客文章中,我们使用了一组固定的提示来评估模型行为如何随时间变化。为此,我们探索和监测了七组指标,以评估模型在性能、偏见、可读性和有害性等不同领域的行为。

我们在这篇博客中简要讨论了结果,但我们鼓励读者自行探索结果!

参考文献

1 — www.engadget.com/chatgpt-100-million-users-january-130619073.html

2- Emily M Bender 等人。“随机鹦鹉的危险:语言模型能否过大?”刊登于:2021 年 ACM 公平性、问责制和透明度会议论文集。2021 年,第 610–623 页(引自第 2 页)。

3 — Hussam Alkaissi 和 Samy I McFarlane. “ChatGPT 中的人工幻觉:对科学写作的影响”。刊登于:Cureus 15.2 (2023)(引自第 2 页)。

4 — Tu, Shangqing 等人。“ChatLog:记录和分析 ChatGPT 随时间的变化。”arXiv 预印本 arXiv:2304.14106 (2023)。arxiv.org/pdf/2304.14106.pdf

5 — cdn.openai.com/papers/Training_language_models_to_follow_instructions_with_human_feedback.pdf

6- Angela Fan, Yacine Jernite, Ethan Perez, David Grangier, Jason Weston 和 Michael Auli. 2019. ELI5: 长篇问答. 收录于第 57 届计算语言学协会年会论文集,第 3558–3567 页,意大利佛罗伦萨。计算语言学协会。

7 — 人对计算机程序员如同女人对家庭主妇?去偏见词嵌入 — doi.org/10.48550/arXiv.1607.06520

8 — Beukeboom, C. J., & Burgers, C. (2019). 刻板印象如何通过语言传播:社会类别与刻板印象交流(SCSC)框架的综述与介绍。传播研究评论,7,1–37. doi.org/10.12840/issn.2255-4165.017

2023 年 8 款最佳数据版本控制工具

原文:towardsdatascience.com/8-best-data-version-control-tools-in-2023-cc9045d37bb8

提供每个数据版本控制工具的优势和劣势的全面概述

Zoumana KeitaTowards Data Science Zoumana Keita

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

--

图片由 Alina GrubnyakUnsplash 上提供

介绍

随着业务需求的不断变化以及数据集的规模和结构的增长,有效跟踪对数据所做的更改变得具有挑战性,这导致了不幸的情况,如数据不一致和错误。

为帮助数据从业者,本博客将涵盖市场上八种顶级数据版本控制工具。将提供每种工具的清晰解释,包括它们的优缺点。

我们为什么需要版本控制我们的数据?

跟踪不同版本的数据可能具有挑战性,就像同时 juggling 多个球一样。没有适当的协调、平衡和精确,事情可能会迅速崩溃。以下几点说明了数据版本控制对任何数据科学和机器学习项目成功的重要性:

存储空间

版本化数据的原因之一是能够跟踪同一数据的多个版本,这些版本显然也需要存储。因此,空间不足会使存储变得困难,这最终会导致失败。

数据审计和合规性

几乎每家公司都面临数据保护法规,如 GDPR,迫使它们存储某些信息以证明合规性和数据来源的历史。在这种情况下,数据版本控制可以帮助公司在内部和外部审计过程中。

实验的存储和可重复性

开发机器学习模型不仅仅是运行代码,更涉及到训练数据和正确的参数。更新模型是一个迭代过程,需要跟踪之前所做的所有更改。在涉及多个用户的更复杂环境中,这种跟踪变得尤为重要。使用数据版本控制可以使每次迭代时能够获得训练数据和实验结果的快照,从而简化实施过程。

上述挑战可以通过使用以下八种数据版本控制工具来解决。

2023 年最佳数据版本控制工具

现在你对博客的期望有了清晰的理解,让我们从 DagsHub 开始,逐一探索它们。

DagsHub

DagsHub 是一个以 GitHub 为基础的集中化平台,允许机器学习和数据科学团队构建、管理和协作他们的项目。除了代码版本控制,团队还可以对数据、模型、实验等进行版本控制。

2022 年发布的 DagsHub 的 直接数据访问(简称 DDA)允许数据科学家和机器学习工程师从 DagsHub 仓库流式传输文件,无需事先下载到本地环境。这可以避免在启动模型训练之前将大量数据下载到本地磁盘。

优点

  • 使用 DDA 无需将所有训练数据拉取到本地磁盘,这可以帮助节省时间和内存存储。

  • 它提供了与 DVC 相同的组织和可重复性,并结合了数据 API 的易用性和灵活性,无需对项目进行任何更改。

  • DDA 使得可以使用 DVC 上传数据并进行版本控制,而无需先将所有数据拉取下来。DagsHub 计算新的哈希值,并代表用户提交新的 DVC 跟踪和修改的 Git 跟踪文件。

缺点

  • 它不能与连接的 GitHub 仓库对接到 DagsHub。

  • 它不支持 'dvc repro' 命令来重现数据管道。

DVC

DVC(Data Version Control 的简称)于 2017 年发布,是由 iterative 创建的开源工具。

DVC 可用于数据和模型的版本控制、实验追踪,以及比较任何数据、代码、参数模型和性能图表。

优点

  • 开源,并兼容所有主要的云平台和存储类型。

  • DVC 可以高效处理大文件和机器学习模型。

  • 作为 Git 的扩展构建,Git 是许多开发者用于源代码版本控制的标准工具。

缺点

  • 在处理非常大的数据集时会失败,因为计算哈希值需要相当长的时间。

  • 与他人的协作需要多个配置,如设置远程存储、定义角色以及为每个贡献者提供访问权限,这可能会令人沮丧且耗时。

  • 添加新数据到存储中需要先提取现有数据,然后计算新的哈希值,再将整个数据推回。

  • DVC 缺乏关键的关系数据库功能,使其不适合那些熟悉关系数据库的人。

Dolt

Dolt 是一个创建于 2019 年的开源工具,用于管理 SQL 数据库,采用类似于 Git 的版本控制。它版本化表格而不是文件,并为这些表格提供 SQL 查询接口。

通过启用数据和结构的同时更改来实现用户体验的提升,使用了版本控制。

优点

  • 它可以像其他 SQL 数据库一样集成到用户现有的基础设施中,并保证 ACID 属性。

  • 大多数开发人员对 Git 进行源代码版本控制非常熟悉。因此,Dolt 与 Git 的集成使其更易于学习。

缺点

  • Dolt 完全依赖于 ACID 属性,这意味着它仅在处理关系数据库时才有用。

  • 对于计算非常大数量的数据(PB 级数据),它没有提供高性能。

  • 由于仅设计用于关系数据库,它不支持图像、音频和自由格式文本等非结构化数据。

Git LFS

Git Large File Storage(Git LFS)是一个由 Atlassian 开发的开源项目,旨在扩展 Git 管理大二进制文件如音频样本、电影和大数据集的能力,同时保留 Git 轻量化的设计和效率。

使用 Git LFS,大文件存储在云中,并通过指针在远程服务器的本地副本中进行引用。

优点

  • 它可以存储任何类型的文件,无论格式如何,这使得它在 Git 上进行大文件版本控制时灵活且多功能。

  • 开发人员可以轻松地将大文件迁移到 Git LFS,而无需更改现有工作流。

缺点

  • Git LFS 需要一个独特的远程 Git 服务器,使其成为单向门。这对于在某些情况下希望返回使用普通 Git 的用户来说是一个劣势。

  • 由于其复杂性,对新用户不够直观。

  • Git LFS 需要一个 LFS 服务器才能工作。这种服务器并非每个 Git 托管服务都提供,在某些情况下需要设置或切换到不同的 Git 提供商。

LakeFS

大多数大数据存储解决方案如 Azure、Google 云存储和 Amazon S3 具有良好的性能,成本效益高,并且与其他工具的连接良好。然而,这些工具在更高级的数据工作流方面存在功能性缺口。

Lake File System(简称 LakeFS)是一个开源版本控制工具,于 2020 年推出,旨在弥合版本控制与大数据解决方案(数据湖)之间的差距。

优点

  • 它支持所有数据格式,无需用户进行任何更改。

  • 它是一个多用户数据管理系统,为各种复杂程度的机器学习管道提供安全的数据摄取和实验环境。

  • 提供 UI 和 CLI 接口,并与所有主要云平台和存储类型兼容。

缺点

  • LakeFS 强烈依赖对象存储,对于其他用例的价值不大。

  • LakeFS 仅用于数据版本控制,这是整个数据科学生命周期的众多部分之一。这意味着在处理数据科学或机器学习管道的其他步骤时,需要集成外部工具。

Neptune

Neptune 是一个用于跟踪和记录 ML 实验和模型的平台。它可以被视为一个集成工具,供机器学习工程师将模型的工件、指标、超参数和 MLOps 过程中的所有元数据存储在一个位置。

优点

  • 直观的协作界面,包括跟踪、比较和组织实验的能力。

  • 与超过 25 个 MLOps 库集成。

  • 提供本地版和托管版两种版本。

缺点

  • 并非完全开源。此外,一个订阅可能足以满足个人使用,但可能会有每月使用限制。

  • 用户需要手动维护离线和在线版本之间的同步。

Pachyderm

Pachyderm 被认为是驱动机器学习生命周期的数据层,通过提供 PB 级的数据版本控制和血统跟踪,以及完全自动扩展和数据驱动的管道。

优点

  • 完全支持结构化和非结构化数据以及任何复杂的领域特定数据类型。

  • 提供社区版和企业版两种版本。

  • 基于容器,并针对主要云服务提供商和本地部署进行了优化。

  • 它具有内置机制来跟踪数据版本和维护数据完整性。

缺点

  • 社区版的管道数量限制为 16 个。

  • 将 Pachyderm 纳入现有基础设施可能会很具挑战性,因为它包含大量技术组件。这也可能使学习过程变得困难。

Delta Lake

Delta Lake 由 Databricks 提供,是一个开源数据湖存储层,运行在现有的数据湖文件系统之上,如 Hadoop 分布式文件系统 (HDFS) 和 Amazon S3。它为数据湖提供 ACID 事务、可扩展的元数据管理和模式强制执行。Delta Lake 支持批处理和流处理,并允许多个并发读写。

优点

  • Delta Lake 为数据湖操作提供了事务性保证,确保数据操作是原子性的、一致的、隔离的和持久的(ACID)。这使得 Delta Lake 在数据湖应用中更加可靠和稳健,尤其是对于那些需要高数据完整性的应用。

  • 它还提供了模式强制功能,确保数据湖中的所有数据结构良好,并遵循预定义的模式。这有助于防止数据不一致、错误以及由数据格式不正确引起的问题。

  • 与 Apache Spark API 的兼容性便利了它与现有的大数据处理工作流的集成。

  • 自动化跟踪和管理不同数据版本减少了信息丢失或数据不一致的风险。

缺点

  • 虽然 Delta Lake 提供了许多强大的功能,但它也为数据湖架构引入了额外的复杂性。

  • 它仅支持有限的数据格式(Parquet),不适用于其他流行的数据格式,如 CSV、Avro、JSON 等。

  • 学习 Delta Lake 并不简单,需要对分布式系统和大数据架构有更深入的理解,以有效管理大规模数据集。

结论

我们涵盖了最佳的 8 种数据版本管理工具,揭示了每种工具的多样化优缺点。虽然一些工具更直观,并在速度和简便性上表现优越,但其他工具则提供了更先进的功能和更大的可扩展性。

总结表(图片由作者提供)

在做出选择时,我建议仔细考虑项目的具体要求,并评估每个选项的优缺点。正确的选择不仅取决于你组织的独特需求和限制,还取决于你的目标。

在你离开之前 🔙

请订阅我的 YouTube 频道 并与朋友分享!

感谢阅读!如果你喜欢我的故事并希望支持我的写作,请考虑 成为 Medium 会员。每月 $5 的承诺将解锁对 Medium 上故事的无限访问权限。

你想请我喝杯咖啡 ☕️ 吗?→ 在这里请我!

随时关注我 在 MediumTwitter,或在 LinkedIn 上打招呼!讨论 AI、ML、数据科学、NLP 和 MLOps 相关内容总是很愉快!

8 个 ChatGPT 提示,用于频繁执行的 Pandas 操作

原文:towardsdatascience.com/8-chatgpt-prompts-for-frequently-done-pandas-operations-e3d7d1965b2c

使用 Pandas 快速完成任务

Soner YıldırımTowards Data Science Soner Yıldırım

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

--

Karsten WinegeartUnsplash 上的照片

我们都听说过 ChatGPT。它不仅在科技行业引起关注,也在更广泛的媒体上成为头条新闻。我经常收到朋友问我是否听说过它。是的,我当然听说过 —— 我每天都在使用它。

尽管有一些关于其在简单任务上表现和可靠性的批评,ChatGPT 和其他大型语言模型(LLMs)在各种任务中表现出色。对我来说,它们大大提高了生产力。

最近,我决定利用 ChatGPT 来进行我定期执行的数据清理和分析的 Pandas 操作。在这篇文章中,我将分享并指导你如何使用 ChatGPT 完成 Pandas 操作的 8 个提示示例。

如果你想了解更多关于 Pandas 的信息,请访问我的课程 500 Exercises to Master Python Pandas

第一个提示以定义其角色:

提示:你是一个 Python 导师,教我 Pandas 库。我会问你如何使用 Pandas 完成特定任务,并希望你向我解释。同时也展示一下代码和你的解释。

在开始提问之前,我已经根据列名和数据类型给 DataFrame 设定了结构。

第二个提示:

提示:首先让我告诉你我拥有的 DataFrame 的信息。然后,我将开始提问。列及其数据类型如下所示,以 Python 字典的形式呈现,键显示列名,值显示数据类型。

{‘store’: dtype(‘O’), ‘product_group’: dtype(‘O’), ‘product_code’: dtype(‘int64’), ‘stock_qty’: dtype(‘int64’), ‘cost’: dtype(‘float64’), ‘price’: dtype(‘float64’), ‘last_week_sales’: dtype(‘int64’), ‘last_month_sales’: dtype(‘int64’)}

我将使用一个我准备的包含虚拟数据的样本数据集。你可以从我的数据集仓库下载。它是名为“sales_data_with_stores.csv”的那个。

这是我为你准备的 DataFrame,方便你参考:

DataFrame 的前 5 行(图片由作者提供)

我们现在可以开始处理 Pandas 任务了。

示例 1

提示:告诉我如何计算每个商店上周的平均销售额。

这是一个简单的一行代码,但如果你刚开始学习 Pandas,它仍然可能需要一些时间来弄清楚。以下是 ChatGPT 的答案:

(图片由作者提供)

我将复制并粘贴代码片段,以防你想自己测试。

# Import pandas
import pandas as pd

# Assuming 'df' is your DataFrame
average_sales = df.groupby('store')['last_week_sales'].mean()

ChatGPT 还提供了详细的解释和代码,说明了任务是如何完成的以及输出的类型。

示例 2

提示:告诉我如何找到上个月每个商店的总利润。

对于这个任务,首先需要计算利润。让我们看看它的回应:

(图片由作者提供)

# Calculate profit per product
df['last_month_profit'] = (df['price'] - df['cost']) * df['last_month_sales']

# Calculate total profit per store
total_profit = df.groupby('store')['last_month_profit'].sum()

示例 3

在处理真实世界的数据时,我们常常会遇到缺失值的挑战。我想看看 ChatGPT 如何处理一个不那么简单的任务。

提示:库存数量列中有一些缺失值。我想用该商店的平均库存数量来替换这些缺失值。告诉我如何完成这个任务。

(图片由作者提供)

# Calculate the average stock quantity per store
average_stock_qty_per_store = df.groupby('store')['stock_qty'].transform('mean')

# Replace missing values in 'stock_qty' with the average stock quantity of that store
df['stock_qty'].fillna(average_stock_qty_per_store, inplace=True)

解决这个任务还有其他方法,但使用 transform 函数是高效的方法之一。至少,这就是我会做到的方式。

我最近写了一篇文章,提到了 transform 函数的重要性。以下是文章中的一个简短片段,解释了我为何会使用 transform 函数来完成这个任务:

这听起来类似于apply函数,但不同于apply函数的是,transform可以返回一个与输入具有相同大小的 Series,这使得它在执行向量化操作和广播时非常有用,尤其是在与 groupby 对象一起使用时。

示例 4

提示:告诉我如何找到上个月每个商店销售额最高的前 3 个产品。

(图片由作者提供)

# First, sort the DataFrame by 'store' and 'last_month_sales'
df_sorted = df.sort_values(['store', 'last_month_sales'], ascending=[True, False])

# Then, group by 'store' and use the apply method to select the top 3 rows for each group
top_3_products = df_sorted.groupby('store').head(3)

它首先对整个 DataFrame 进行排序。我们还可以在每个组内进行排序。让我们看看 ChatGPT 是否知道如何做到这一点。

示例 5

提示:是否有另一种方法可以在不先对 DataFrame 进行排序的情况下完成这个任务?

(图片由作者提供)

def top_3_products(group):
    return group.nlargest(3, 'last_month_sales')

top_3_df = df.groupby('store').apply(top_3_products)

需要注意的是,首先对整个 DataFrame 进行排序可能是更高效的方法。我只是想看看 ChatGPT 是否提供了不同的解决方案。

示例 6

提示:我想查看每家商店上周的表现与上个月的表现比较。

这是一个简单的任务,但提示没有直接告诉 ChatGPT 怎么做。让我们看看它的表现如何。

(图片由作者提供)

# Calculate total sales for each store in the last week and last month
total_sales_last_week = df.groupby('store')['last_week_sales'].sum()
total_sales_last_month = df.groupby('store')['last_month_sales'].sum()

# Create a new DataFrame to store this information
performance_df = pd.DataFrame({
    'LastWeekSales': total_sales_last_week,
    'LastMonthSales': total_sales_last_month,
})

# Calculate the change in sales from last month to last week
performance_df['SalesChange'] = performance_df['LastWeekSales'] - (performance_df['LastMonthSales'] / 4)

作为对上周表现与上个月相比的回应,它计算了上周销售额与上个月销售额的四分之一之间的差异,这很有意义。

它创建的 DataFrame 看起来是这样的:

示例 7

提示:创建一个名为 critical_stock 的新列,如果 stock_qty 小于 500,则值为 1,否则为 0。

ChatGPT 为这个任务提供了 3 个选项。这是我比较喜欢的两个:

(图片由作者提供)

# first option
df['critical_stock'] = df['stock_qty'].apply(lambda x: 1 if x < 500 else 0)

# second option
import numpy as np

df['critical_stock'] = np.where(df['stock_qty'] < 500, 1, 0)

示例 8

提示:创建一个 DataFrame 的摘要,包括大小、缺失值、数据类型和列统计信息。

用于总结 DataFrame 的函数和方法相当简单,但 ChatGPT 做得很好,将它们整理得非常整洁。

我只是复制粘贴了它生成的代码:

# Import necessary libraries
import pandas as pd

# DataFrame size
print("DataFrame Size:")
print(df.shape)
print("\n")

# Missing values
print("Missing Values:")
print(df.isnull().sum())
print("\n")

# Data types
print("Data Types:")
print(df.dtypes)
print("\n")

# Column statistics
print("Column Statistics:")
print(df.describe(include='all'))

最后的话

我们讨论的示例简要展示了 ChatGPT 如何彻底改变我们在数据分析中执行常见任务的方式,特别是在 Pandas 上。它不仅提高了我们完成任务的效率,还丰富了我们对 Pandas 复杂操作的理解。

我将探索更多利用 ChatGPT 的方法来完成更复杂的任务。需要注意的是,随着任务复杂性的增加,你编写提示的方式变得尤为重要。这就是为什么有一个新的学科正在出现,叫做提示工程。

你可以成为 Medium 会员 来解锁对我写作的完整访问权限,以及 Medium 其余的内容。如果你已经是会员,别忘了 订阅 以便每当我发布新文章时能收到电子邮件。

感谢阅读。如果你有任何反馈,请告诉我。

我没有放弃成为数据科学家的梦想的 8 个理由,以及你也不应该放弃的原因

原文:towardsdatascience.com/8-reasons-why-i-didnt-give-up-on-my-dream-to-be-a-data-scientist-why-neither-should-you-4f32b73f2473

数据科学家在科技领域的头几年编年史

当困难时期来临时,你可以回顾的一份清单

Khouloud El AlamiTowards Data Science Khouloud El Alami

·发布在 Towards Data Science ·阅读时间 8 分钟·2023 年 7 月 14 日

--

我已经在 Spotify 担任数据科学家 2 年了,我可以说,从商业转行到数据科学是我做出的最佳决定之一。

数据科学可以是一个令人惊叹的职业选择。到达那里是另一回事。

成为真正的数据科学家需要付出大量的努力和决心。为了确保你能成功,你首先需要明确你的为什么,然后再解决怎么做的问题。

前方的旅程漫长,但一切都是值得的! — 图片由作者(Midjourney)提供

但首先,让我给你讲一个小故事

如果你读过我之前的文章,你知道这次职业转型远非轻松。

早期的旅程变成了过山车,这让我意识到我真的需要理清我的为什么

仅仅因为我做出了这个职业选择,并不意味着我总是充满了动力和决心。

当你背负着 6 万美元的学生贷款时,失败的意义变得截然不同。它与最深处的恐惧交织在一起。失败不是一种选择。成功是唯一的选择。

然后有一天……

在 2020 年 3 月,我感到处于最低谷,因为我快要在纽约大学的 6000 美元课程中失败了,我不知道自己要如何摆脱困境。焦虑开始猛烈地袭击我。

我可以选择站起来为自己争取,或者放弃。我选择了第一个选项。

然后我列了一个清单。确切地说,就是这个 ⬇

我把这条笔记保留得和 2020 年写的一模一样。

我也不想过多剧透 😌。如果你想知道列表的内容,你只有继续阅读(哈哈哈 😈)

这是我第一次后来真的查看的列表!

(相对于我们把它们埋在手机角落里那些笔记)

我列出了所有理由,说明为什么这一信念的跳跃是值得所有这些挣扎的。我会在每次感觉到动力减退或自我怀疑袭来时查看它。

这个列表给了我每次需要的动力,让我坚持自己的梦想。我仍然哭了,哈哈,但至少我在完成作业的同时哭了。

在这篇文章中,我将与您分享我认为数据科学是最令人兴奋的职业路径之一的 8 个理由。列出我认为这一切麻烦都是值得的原因。

请记住,这个列表基于我的 个人 兴趣和愿望。其中的一些元素可能不适合每个人。但既然这些对我帮助如此之大,我仍然觉得它们值得分享。

1. 成为数据科学家就像是现代的福尔摩斯

我在之前的文章中已经谈到过这个问题,但对于那些没有读过的人,这个理由最接近我内心。

侦探们每天都利用知识来解决难题。他们不懈地调查,追寻真相,通过解决最复杂的谜题来寻找答案。

这正是数据科学家所做的工作。

作者提供的图片(Midjourney)

这可能感觉像是在浪漫化这一切,但这确实是我当时的感受,也是我现在的感受。我们每天都在玩弄数据,以揭示它隐藏的机会。这需要培养敏锐的问题解决能力🔎 和在追寻真相的过程中坚持不懈的精神。

每一个新项目的启动都可能是不同的。这就像是面对一个新的案件待解决。作为数据专家意味着你可以找到有趣且新颖的方法来提供只有 能揭示的答案。

诚然,我不是每天都在追捕犯罪分子来解决案件。相反,我是在追逐数字,让它们吐露并呈现真相。对我来说,这已经足够了。

2. 数据科学是最具多样性的职业之一

我在数据科学学位期间结交的所有朋友都来自完全不同的背景。从天体物理学到政治科学(以及更多)。他们现在都是在完全不同的行业工作的数据科学家。有些人在开发抗癌模型,另一些则在帮助游戏开发者提供最佳体验。

这是一个备受追捧的职业路径的原因之一,它允许你调和你的职业抱负与个人兴趣。 数据科学给了你在你选择的领域工作的机会。当你对当前行业不再满意时,你还可以更换行业。

我自己就是一个很好的例子。

当我在攻读学位时,除了 Spotify,我无法想象自己在其他公司工作。音乐深深触动了我,以至于我在晚年才开始学习小提琴,并在自己内心发现了新的激情。

现在,我能够将我对数据的终身兴趣与在 Spotify 的职业生涯结合起来。

不论你是在观星还是救援中找到乐趣,数据科学为你打开了与终身热情相关的工作大门。

3. 数据科学是当今最相关的科学之一

10 年前它是相关的,而今天它变得更加重要。数据无处不在,它的重要性随着时间的推移呈指数增长。

根据 Statista 的数据,全球生成和消费的数据量从 2010 年的 2 泽字节增加到 2023 年的 120 泽字节 (1 泽字节 = 10 亿兆字节) [1, 2]。所以“相关”这个词简直是轻描淡写。数据正在爆炸

你认为谁在通过 AI 创新改变科技世界?当然是数据科学家。

所以如果今天这个词可以有不同的含义,那么你想成为的那种数据科学家真的是由你决定的。如果你想乘上 AI 的浪潮,你可以。如果你更愿意调查数据并解开谜团,你也可以。

不论你乘风破浪,你在就业市场上依然会保持相关性。

4. 数据科学可以改变人们的生活

如果你通过改变世界和产生积极影响来寻求生活的意义,那么数据科学可能是适合你的道路。

当你成为数据科学家时,你可以选择成为更大事物的一部分。由于可以选择的行业范围非常广泛,你甚至有机会参与其中。

我曾经收到过在联合国核部门担任数据科学家的工作邀请。这有多酷?

当你成为数据科学家时,天际线就是你的极限(我说这话不是为了显得老套)。能够成为更大事物的一部分并产生影响是无价的。至少对我来说是这样的。

5. 数据科学家是最受需求的人群之一

由于数据如此普遍,数据科学家将永远是必需的。

几乎每家公司今天都会收集数据。无论它们是初创公司还是成熟企业,它们肯定会在某个时刻需要有人从这些无意义的数据中提取价值。

为什么? 因为如果他们想要继续在游戏中保持领先,他们需要开始挖掘隐藏得最深的价值。这就是数据科学家的作用 他们帮助挖掘出公司保持领先所需的竞争优势。

当你成为数据科学家时,你的组织依赖于你通过发现机会和技术创新来帮助他们成长。他们会把重要的责任托付给你,这意味着你的贡献将被高度重视,你的工作也同样如此。

6. 数据科学极具声望

任何声称成为数据科学家需要 12 个月甚至 6 个月的人,显然遗漏了重要数据。你需要的技能集需要时间来培养,不能一夜之间学会。

如今,我们看到许多人声称自己是数据科学家,这可能使入门门槛看起来很低。但相信我,真正的数据科学家成为自己的过程经历了多年。那些成功的人不一定是最聪明或最有才华的,但他们更可能是最有驱动力和韧性的人。

成为数据科学家的道路既具有挑战性又非常有回报。所以如果你准备好开始这段冒险,欢迎加入,并做好准备,你将经历一段旅程。

7. 数据无处不在,甚至在你的个人生活中

而且,我指的是几乎所有地方。从你拥有设备的那一刻起,你已经开始收集数据。即使不在公司工作,你也可以玩弄数据。

我见过一些人收集自己的健康数据,并尝试解读这些数据以了解健康问题背后的因素。即使在个人生活中,懂得如何处理数据也是有用的。

无论是建模你的日常习惯还是理解你在 Strava 上的统计数据,了解一些数据知识将对你的生活有益。

8. 数据科学处于第四次工业革命的前沿

这意味着你可以参与塑造明天的世界。

数据科学令人兴奋,因为它在智力上的刺激性和新颖性。你可以学习到各种具有挑战性的技术,掌握这些技术意味着你将能够解答一些最具挑战性、最令人兴奋和神秘的问题。这是一个终身学习的旅程。

这个领域在不断变化和发展,它带着你一起向未来迈进。兴奋感有保障。

给你的建议:制定你自己的清单

我希望我的清单能激励你制定自己的理由清单,说明你为什么要做这件事。

这些理由将触动你的内心,而不仅仅是你的大脑(或你的钱包)。因为当你跌入谷底时,只有这些理由才能真正引起你的共鸣。

我为你准备了礼物🎁!

订阅我的通讯K’s DataLadder,你将自动获得我的终极 SQL 备忘单,其中包含我在大科技公司工作中每天使用的所有查询 + 另一个神秘礼物!

我每周分享成为数据科学家在科技领域的经验,以及实用的技巧、技能和故事,这些都是为了帮助你提升自己——因为没有人真正知道,直到他们亲身经历!

如果你还没有这样做的话

  • 订阅我的YouTube频道。新视频很快就会发布!

  • 关注我在InstagramLinkedInX上的动态,随你选择。

再见!

8 种建模季节性的方法

原文:towardsdatascience.com/8-techniques-to-model-seasonality-2f81d739710

如何处理季节性以进行预测

Vitor CerqueiraTowards Data Science Vitor Cerqueira

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

--

照片由 Clark Young 拍摄,来源于 Unsplash

本文是对 上一篇帖子 的后续。那里,我们识别了 3 种季节性模式。

在这里,我们将:

  • 学习如何描述时间序列的季节性。

  • 了解 8 种你可以用来建模季节性的方法。

建模季节性模式

季节性指的是在某些时间段内重复出现的模式。它是建模中一个重要的变异来源。

时间序列及其季节性调整版本。数据来源在下一部分。图像由作者提供。

处理季节性的方式有很多。一些方法在建模之前去除季节性成分。季节性调整数据(时间序列减去季节性成分)突显长期效果,如趋势或商业周期。其他方法则增加额外的变量,以捕捉季节性的周期特性。

在讨论不同的方法之前,让我们创建一个时间序列,并描述其季节性模式。

分析示例

我们将使用与 上一篇文章 相同的过程(另见参考文献 [1]):

period = 12 # monthly series
size = 120

beta1 = np.linspace(-.6, .3, num=size)
beta2 = np.linspace(.6, -.3, num=size)
sin1 = np.asarray([np.sin(2 * np.pi * i / 12) for i in np.arange(1, size + 1)])
cos1 = np.asarray([np.cos(2 * np.pi * i / 12) for i in np.arange(1, size + 1)])

xt = np.cumsum(np.random.normal(scale=0.1, size=size))

yt = xt + beta1 * sin1 + beta2 * cos1 + np.random.normal(scale=0.1, size=size)

yt = pd.Series(yt)

这是本系列的样子:

人工时间序列与随机平稳季节性。图像由作者提供。

我们可以通过其强度来描述季节性模式:

# https://github.com/vcerqueira/blog/tree/main/src
from src.seasonality import seasonal_strength

seasonal_strength(yt, period=12)
# 0.90

结果是 0.90,这表明季节性确实很强

该时间序列的自相关图如以下图所示:

自相关最多到 24 个滞后期。图片作者提供。

自相关显示出振荡行为。在第一个季节滞后期(12)上有显著的峰值。这些模式确认了季节性的相关性。

最后,我们使用 Canova-Hansen 测试检查季节性单位根:

from pmdarima.arima import nsdiffs

nsdiffs(x=yt, m=period, test='ch')
# 0

结果是 0,这表明没有季节性单位根。因此,我们可以得出结论,季节性模式是平稳的。

那么,我们如何处理像这样的季节模式呢?

8 种建模季节性的技术

在本文的其余部分,你将学习 8 种建模季节性的技术。

1. 季节虚拟变量

季节虚拟变量是一组二进制变量。这些变量表示观测值是否属于某个特定周期(例如,1 月)。

这是一个创建这些变量的示例:

from sktime.transformations.series.date import DateTimeFeatures
from sklearn.preprocessing import OneHotEncoder

monthly_feats = DateTimeFeatures(ts_freq='M',
                                 keep_original_columns=False,
                                 feature_scope='efficient')

datetime_feats = monthly_feats.fit_transform(yt)
datetime_feats = datetime_feats.drop('year', axis=1)

encoder = OneHotEncoder(drop='first', sparse=False)
encoded_feats = encoder.fit_transform(datetime_feats)

encoded_feats_df = pd.DataFrame(encoded_feats,
                                columns=encoder.get_feature_names_out(),
                                dtype=int)

这段代码生成了以下数据。

季节虚拟变量在独热编码前后的效果。图片作者提供。

首先,我们获取每个观测值的季度和月份信息(左侧表格)。这些信息存储在datetime_feats对象中。然后,我们使用独热编码创建虚拟变量(右侧表格)。

如果季节性是确定性的,季节虚拟变量特别有效。如果我们不期望季节模式在强度和周期性上发生变化。

我们可以检查季节虚拟变量的系数,以分析季节效应及其变化。这对模型的解释性很有帮助。

季节虚拟变量的缺点是它们假设不同的周期是独立的。1 月的观测值与 12 月的观测值相关。但虚拟变量对这种相关性视而不见。因此,如果季节模式发生变化,虚拟变量可能不是最佳选择。

2. 傅里叶级数

示例正弦和余弦项。图片作者提供。

傅里叶级数是基于正弦和余弦波的周期性和确定性变量。与季节虚拟变量相反,这些三角函数将季节性建模为周期性模式。这种结构更能反映现实。

这种方法在 sktime Python 包中实现:

from sktime.transformations.series.fourier import FourierFeatures

fourier = FourierFeatures(sp_list=[12],
                          fourier_terms_list=[4],
                          keep_original_columns=False)

fourier_feats = fourier.fit_transform(yt)

你需要指定两个主要参数:

  • sp_list:作为列表的季节性周期(例如,月度数据的 12)

  • fourier_terms_list:项数,指包含的正弦和余弦级数的数量。这些项会影响表示的平滑度。

在实践中,傅里叶级数是你可以添加到模型中的解释变量。例如,你可以将这些与滞后特征结合使用。

3. 径向基函数

3 个示例径向基函数。图片作者提供。

径向基函数(RBF)是傅里叶级数的替代方法。其思想是创建重复的钟形曲线来模拟重复的模式。

RBF 在scikit-lego Python 包中通过RepeatingBasisFunction方法实现:

from sklego.preprocessing import RepeatingBasisFunction

rbf_encoder = RepeatingBasisFunction(n_periods=4,
                                     column='month_of_year',
                                     input_range=(1, 12),
                                     remainder='drop',
                                     width=0.25)

rbf_features = rbf_encoder.fit_transform(datetime_feats)
rbf_features_df = pd.DataFrame(rbf_features,
                               columns=[f'RBF{i}'
                                        for i in range(rbf_features.shape[1])])

这种方法的三个最重要的参数是:

  • n_periods:要包含的基函数数量

  • input_range:我们想要创建基函数的列的输入范围。例如,在上述示例中,我们使用(1, 12),即月份的范围;

  • width:径向基函数的宽度,控制其平滑度

与傅里叶级数一样,RBF 变量可以用作模型中的解释变量。

4. 季节性自回归

自回归是大多数预测模型的基础。其思想是利用最近的过去观测值(滞后)来预测未来值。这个概念可以扩展到建模季节性。季节性自回归模型包括相同季节的过去值作为预测变量。

SARIMA 是一种应用这个思想的流行方法:

import pmdarima as pm
model = pm.auto_arima(yt, m=12, trace=True)

model.summary()
# Best model:  ARIMA(0,1,0)(1,0,0)[12]

在前面的代码中,我们运行了auto_arima来找到 ARIMA 的最佳配置。得到的模型包括相同季节的第一个滞后作为输入。

使用季节性滞后作为解释变量是建模季节性的有效方法。请注意,使用这种方法时应处理季节性单位根。你可以像以前一样使用nsdiffs和 Canova-Hansen 测试,并在必要时使用季节性差分。

5. 添加额外变量

像季节性虚拟变量或傅里叶级数等方法捕捉重复的模式。但是,这些方法只是季节性原因的代理。

外生变量,如温度或每月的工作天数,可以用来建模季节性。

每日平均温度时间序列。作者提供的图像。数据来源于参考文献[2]。

你可以通过使用ARDL 公式将这些变量纳入模型。

6. 通过季节性差分进行调整

我们可以通过在建模之前从数据中去除季节性来处理季节性。这可以通过季节性差分或分解来完成。

季节性差分是对同一季节连续观测值之间进行差分的过程。此操作特别有用,以去除季节性单位根。

你可以使用diff方法进行季节性差分:

from sklearn.model_selection import train_test_split
from sktime.forecasting.compose import make_reduction
from sklearn.linear_model import RidgeCV

train, test = train_test_split(yt, test_size=12, shuffle=False)

train_sdiff = train.diff(periods=12)[12:]

forecaster = make_reduction(estimator=RidgeCV(),
                            strategy='recursive',
                            window_length=3)

forecaster.fit(train_sdiff)
diff_pred = forecaster.predict(fh=list(range(1, 13)))

上面,我们还在差分系列上建立了一个 Ridge 回归模型。通过恢复差分操作,你可以获得原始尺度上的预测值。

7. 通过分解进行调整

你也可以使用时间序列分解方法,如 STL,来去除季节性。

差分和分解有什么区别?

差分和分解方法都用于去除时间序列中的季节性。但,变换后的数据建模方式不同。

在应用季节性差分时,模型在差分数据上工作。你需要恢复差分操作以获取原始尺度上的预测。

使用基于分解的方法,你需要两个预测集。一个是季节性部分,另一个是季节调整后的数据。最终的预测是各部分预测的总和。通常,季节性朴素方法用于预测季节性成分。

这是一个基于分解方法的工作示例:

from statsmodels.tsa.api import STL
from sktime.forecasting.naive import NaiveForecaster

# fitting the seasonal decomposition method
series_decomp = STL(yt, period=period).fit()

# adjusting the data
seas_adj = yt - series_decomp.seasonal

# forecasting the non-seasonal part
forecaster = make_reduction(estimator=RidgeCV(),
                            strategy='recursive',
                            window_length=3)

forecaster.fit(seas_adj)

seas_adj_pred = forecaster.predict(fh=list(range(1, 13)))

# forecasting the seasonal part
seas_forecaster = NaiveForecaster(strategy='last', sp=12)
seas_forecaster.fit(series_decomp.seasonal)
seas_preds = seas_forecaster.predict(fh=list(range(1, 13)))

# combining the forecasts
preds = seas_adj_pred + seas_preds

在这个示例中,我们建立了一个岭回归模型来预测季节调整后的数据。一个季节性朴素模型预测季节性成分。然后,将两个预测结果加在一起。

8. 动态线性模型(DLM)

回归模型的参数通常是静态的。它们不会随时间变化或是时间不变的。DLM 是线性回归的一种特殊情况。其主要特点是参数随时间变化,而不是静态的。

DLM 认为季节性时间序列的结构会随季节变化。因此,一个合理的方法是建立一个具有时间变化参数的模型。即季节性变化的参数。

书籍第十五章 在参考文献[1]中提供了这种方法的一个简洁 R 示例。他们使用了时间变化的 MARSS(多变量自回归状态空间)方法来建模变化的季节性。

关键要点

在这篇文章中,你了解了 8 种不同的季节性建模方法。这些方法包括:

  • 季节性虚拟变量

  • 傅里叶级数

  • 径向基函数

  • 季节性自回归

  • 外生变量

  • 季节性差分

  • 季节性分解

  • 季节性参数变化的动态模型

我们以具有随机平稳季节性的时间序列为例。然而,有些方法可能并不适合这种类型的季节性。你应该测试不同的方法,选择适合你的数据的一种(或多种)方法。

感谢阅读,下一篇故事见!

相关文章

代码

参考文献

[1] Holmes, Elizabeth E., Mark D. Scheuerell, and E. J. Ward. “Applied time series analysis for fisheries and environmental data.” NOAA Fisheries, Northwest Fisheries Science Center, Seattle, WA (2020).

[2] 来自美国爱达荷州西南部四个以西部刺柏为主的实验流域的天气、雪和流量数据。 (许可证:美国公共领域)

[3] Sims, Christopher A. “回归中的季节性。” 《美国统计学会期刊》 69.347 (1974): 618–626。

攻读数据科学硕士学位前你必须考虑的 8 件事

原文:towardsdatascience.com/8-things-you-must-consider-before-committing-to-a-data-science-masters-degree-d7b22f6ee455

别急着用你的 Pinterest 板块:回到大学对你来说是否是正确的选择?

Matt ChapmanTowards Data Science Matt Chapman

·发布在 Towards Data Science ·10 分钟阅读·2023 年 4 月 14 日

--

图片由亚历山大·施梅克提供,发布在Unsplash

近年来,数据科学相关的硕士项目数量激增。根据 FindAMasters.com 的数据[1],全球范围内的大学目前提供超过 3,500 个与数据科学和人工智能相关的硕士学位。

显然,这一领域有很大的需求,大学擅长利用这一点。几乎每天我都会被互联网的神灵用一些华丽的、营销到位的大学广告轰炸,谈论“数据是新的石油”。

如果你在考虑攻读该领域的硕士学位,理性评估入学的利弊可能会非常困难。显然,大学会说这是一个好主意:他们希望得到你的钱!但这真的是一个好主意吗?尤其对来说,这是否真的是一个好主意?

我写这篇文章是因为我想帮助你做出明智的决定,并考虑所有重要的问题。我自己确实拥有数据科学硕士学位,虽然我认为这对我来说是值得的时间和金钱投资,但我也相信硕士学位并不适合所有人。我的希望是这些 8 个考虑因素能帮助你判断这是否对来说是正确的选择。

你为什么想攻读硕士学位?

首先,你需要搞清楚是什么驱动你考虑攻读数据科学硕士学位。

对,我说的是:不是来自科罗拉多州的二年级学生鲍勃,他在 r/datascience 上写过帖子;也不是来自班加罗尔的阿尼鲁德,他在 Quora 上写过关于从软件工程转行的帖子。。每个人的理由都不同,那么你的理由是什么?

我从这一点开始,是因为你的动机会极大地影响你应听取的建议。如果你没有明确的动机,就无法过滤掉不相关的建议,最终会阅读大量无法解决你特定问题的垃圾信息。

从我所见,最常见的动机有:

  • 职业相关:例如,获得(潜在)雇主的信誉,提高获得工作或晋升的机会。

  • 学习相关:你希望尽可能快地学习更多内容。也许你尝试过在晚上和周末学习,但进展缓慢让你感到沮丧。对你来说,主要的动力是能够“全力以赴”,快速学习。

  • 智力好奇心:也许你已经有了 AI 方面的基础知识,但有一个特定的领域或话题你非常想探索。对你而言,硕士学位的主要吸引力是有机会从生活中抽身,专注于探索自己的兴趣。

  • 责任感:你知道自我驱动学习并不总是一致的,所以你需要一个切实的目标(比如硕士学位),这会让你在学习中保持责任感和一致性。

  • 冒名顶替综合症:你已经在该行业有了一份工作,但没有正式的数据科学背景,想要通过官方资格来获得验证。

这些感觉与你产生共鸣吗?类似的想法是否曾经出现在你的脑海中?在你对自己的驱动力有明确的了解之前,很难公平地评估是否应该报名。

对我而言,我的动机是职业和学习相关原因的结合。在决定报名之前,我与许多高级数据科学家交谈,他们都告诉我,获得硕士学位对于从事数据科学职业并非客观上必要。我也已经有了良好的本科学位(因此我不需要向雇主证明我的学术能力),而且我看到有足够的在线数据科学短期课程可以让我自学硕士课程中的内容。然而,作为一个完全没有 AI 背景的人(我之前的工作是在销售和市场营销领域),我知道自己会从一个密集的学习阶段中受益,感觉来牛津大学是一个难得的机会。

这是必要的吗?

一旦你确定了为什么考虑攻读硕士学位,你需要问自己:硕士学位是否真的对实现我的目标是必要的?

例如,假设你攻读硕士学位的主要动机是获得领域内的信誉,并增加获得工作/晋升的机会。你能否确定硕士学位会对此有所帮助?我知道它可能(理论上)会,但是否真的会?

很可能,答案不像你想象的那么简单:这要看情况。例如,如果你已经有了一个与 STEM 相关的学位,那么获得硕士学位可能对提升你的就业机会帮助不大。这在你已经拥有如硕士或博士这样的高层次资格时尤为明显。这些资格已经展示了你对数字的能力,而另一个学位可能不会在潜在雇主眼中增加太多的价值。相反,你职业发展的限制因素可能仅仅是你在数据/分析方面缺乏商业经验。在这种情况下,你不如尝试弥补简历中的“经验”空白,例如获得实习或较低级别的工作。

我的观点是,你不应该假设获得硕士学位会显著帮助你的职业发展。判断它是否必要的简单方法是与公司/行业中你特别感兴趣的人员交谈。试试看:联系一些你希望工作公司的高级/首席数据科学家,询问他们是否认为硕士学位是进入这些公司工作的前提条件。

正如我之前讨论的那样,在我看来,获得硕士学位是有帮助的,但我不会说它是必要的。这对你来说是否也是如此?

这是实现你目标的最有效方式吗?

接下来的问题是,你是否将这个学位视为时间的有效利用。一开始,这可能看起来是个奇怪的问题。我们认为,全日制学习是快速学习大量知识的最佳方式?然而,问题是,尽管硕士学位似乎是实现这一目标的明显途径,但它并不是唯一的方式,甚至可能不是最有效的。在我的硕士期间,例如,必须修读许多与数据科学不直接相关的课程(例如,调查设计、互联网社会学等)。不要误解我的意思:这些课程非常有趣,为我的作品集提供了很好的素材,我喜欢这种在大学之外难以获得的广泛学术体验。但如果你有非常具体的目标(例如,成为 NLP 领域的专家),那么像硕士这样的广泛资格可能不是一个很有效的时间投资方式。

机会成本是什么?

考虑以下问题:如果你不攻读硕士学位,你可以做什么替代的事情?而且,关键是,那些替代方案在帮助你实现领域内长期目标方面是否实际上有帮助?

图片由Raquel Martínez提供,来源于Unsplash

考虑机会成本是评估不同机会利弊的一个非常有用的经验法则。具有讽刺意味的是,我在完成硕士学位时遇到了这个例子,当时我们的一个教授鼓励我们考虑将学习延续到博士学位。他的论点大致是这样的:雇主对博士学位的评价远高于硕士学位,所以如果我们想获得顶级职位,我们应该强烈考虑将学习延续几年,争取博士学位。

然而,这实际上并不公平,因为如果博士学位需要 4 年时间完成,那么我们实际上应该把博士学位与硕士学位加 3 年数据科学工作经验进行比较。突然之间,博士学位是否是一个好主意变得不那么明显,因为 3 年的工作经验在人工智能领域中对候选人简历的提升是巨大的。

不要误解我的意思;博士学位可能仍然是某些工作的前提条件。我想表达的是,你需要考虑所有选项并公平比较。一个简单的方法是把你的时间视作一种可以投资的资源。简单问自己:我如何投资这项资源以获得最大的投资回报?

你能负担得起吗?

不要假设你通过获得更高薪资的工作来“弥补”硕士学位的成本。我知道很多做了数据科学相关硕士的人仍在寻找工作。尽管你可能会在网上读到不同的意见,但并不能保证你会找到一份高薪工作,所以不要假设花在硕士学位上的钱会通过工作得到报销(至少在短期内)。

图片由 Towfiqu barbhuiya 提供,来源于 Unsplash

如果资金是个问题,有很多方法可以解决这个问题。你可以选择兼职学习(即,同时工作),申请奖学金,或者选择攻读资助的博士学位,或者让雇主赞助你的学习;我知道几个人就是这样做的。

在我的情况下,我采取了几种不同的方法。首先,我申请了尽可能多的奖学金,并获得了一个部分奖学金,覆盖了我费用的一大部分。我在攻读硕士学位之前也工作了几年,以便有时间积攒学费。由于大额的财务支出,我也专注于找工作,确保在年初就开始申请,并尽量缩短完成学位后的“失业”时间。

这是一个好的课程吗?

不要以为仅仅因为它是知名大学,课程就一定好。密切关注具体的模块和授课教师。了解课程的学生满意度评分,并查看毕业生的就业情况。如果你找不到关于这些内容的官方统计数据,可以看看是否有开放日或在线体验活动。考虑我的硕士课程时,我向课程协调员提出了一些问题。我认识的其他人则通过 LinkedIn 联系了在读学生和校友以获取他们的意见。你问谁并不重要,但问题越具体,效果越好。

这个课程适合你吗?

数据科学课程的内容和风格差异很大,特别是在编码先决条件方面。有些课程针对新手编码者,而另一些则期望有更高水平的编码经验。

重要的是找到适合你水平的课程。例如,我在牛津大学的硕士课程尝试(在我看来,成功了!)满足各种学术背景的人,但这意味着最初的几周有点像“编码速成课程”,如果你已经有大量的编码经验,那么这可能会很无聊,甚至有些浪费时间。

还要考虑其他因素,比如课程是授课型硕士还是研究型硕士,是否提供工作实习机会,是否在线/远程,是否全日制或兼职。所有这些因素都会影响它是否适合你。在我的情况下,我有一点编码经验,但真的想要一个全面涵盖数据科学教育各个方面的课程。鉴于我有社会科学和商业背景,我还希望选择一个专注于将人工智能应用于经济学和商业问题的课程,而不是一个纯理论的课程,这样的课程会忽略人工智能的商业和社会方面。

你确定你真的想做数据科学吗?

图片由 Vladislav Babienko 提供,来自 Unsplash

如果你对是否想从事数据科学/人工智能工作犹豫不决,那么硕士学位并不是了解的最佳方式。当然,它是一种方式。但它也是最昂贵和耗时的方式之一。

我认为很多人攻读硕士学位是为了推迟关于未来的决定。不要这样做!不如选择一个间隔年,获得一些实际工作经验,以了解你是否喜欢日常工作。

最终,数据科学并不适合每个人,考虑到数据科学可能不是你合适的路径,这一点很重要。你可能更适合其他相关领域,比如产品管理、软件工程、设计、人机交互、研究、统计学、数据可视化、分析翻译等。相关的职业有很多,你不应该因为对它们了解不多而排除那些不那么知名或不那么热门的选项。在决定攻读硕士学位之前,我尝试了几份不同的工作,我认为这些经历确实帮助我明确了自己想进入人工智能领域。此外,一旦你完成了硕士学位,拥有一些商业经验将有助于你顺利进入下一份工作。

结论

我真的希望这篇文章能为你的思考带来一些清晰。如果你喜欢这篇文章,关注我将对我非常重要,因为这有助于支持我的写作。如果你有任何反馈,请在评论中告诉我!

来源

[1] FindAMasters.com www.findamasters.com/masters-degrees/?Keywords=data+science [访问日期:2023 年 4 月 12 日]

高效数据可视化的 8 个技巧

原文:towardsdatascience.com/8-tips-for-effective-data-visualisation-f00e711b164a

数据 | 可视化 | 分析

一份正确向观众展示你的见解和观察的指南

David FarrugiaTowards Data Science David Farrugia

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

--

图片由 Luke Chesser 提供,来源于 Unsplash

当我们讨论数据科学时,我们往往过于关注数据清理和机器学习方面的过程。

讨论的主要点似乎是如何最佳地准备我们的数据集以进行建模,我们需要工程化并包含哪些特征在我们的训练中,我们会首先尝试哪种机器学习技术,以及我们将如何评估它?

虽然这些都是有效且重要的问题需要询问和规划,但作为数据科学家,我们常常忘记优先考虑任何项目的最大卖点之一:可视化

每一个数据科学项目至少涉及两个方面:技术方面(即数据科学家)和非技术方面(即利益相关者,可能是一些经理或高层主管)。

我们需要记住,数据科学的根本目的是提升业务价值。大多数人不理解数据。我们必须展示给他们。

当做得有效时,数据可视化可以帮助我们发现洞察、识别趋势并传达复杂的想法。

在多年的经验中,我发现这一领域是许多专业人士的短板——尤其是那些初级职位的人员(包括我自己!)。

创建优秀的数据可视化是另一项独立的技能。数据可视化很容易造成更多的混淆而不是清晰。

在这篇文章中,我们将讨论 8 个关于如何生成美观、易于解释和有效的数据可视化的技巧。

提示 #1:选择合适的图表类型

迄今为止,最难掌握的技能是选择合适的可视化类型的直觉。

我们有柱状图、折线图、饼图、散点图、热图和小提琴图——仅举几例。很容易迷失方向并感到不知所措。前往 seaborn 画廊,你会立即开始理解这一决策有多么广泛。

[## 示例画廊 - seaborn 0.12.2 文档

编辑描述

seaborn.pydata.org](https://seaborn.pydata.org/examples/index.html?source=post_page-----f00e711b164a--------------------------------)

正如预期的那样,这可能是我经常看到的最常见错误之一。使用不正确的数据可视化图表。

选择正确的图表类型至关重要,这直接与我们展示的数据类型和想要传达的信息相关。

假设我们有一个小数据集,显示了一个商店本月售出了多少苹果、香蕉和橙子。

# Example data
data = {'apples': 10, 'bananas': 5, 'oranges': 7}

让我们深入探讨不同图表类型如何传达信息。

在所有情况下,我们都需要导入以下包:

import matplotlib.pyplot as plt
import pandas as pd

柱状图

# Bar chart
plt.bar(data.keys(), data.values())
plt.title('Fruit Sales')
plt.xlabel('Fruit')
plt.ylabel('Number of Sales')
plt.show()

作者提供的图像

柱状图在显示每个类别(在我们的例子中是水果类型)的值方面做得非常好。这个图表清楚地显示出最畅销的水果是苹果,最不畅销的是香蕉。

折线图

# Line chart
df = pd.DataFrame(data, index=[0])
df.plot.line()
plt.title('Fruit Sales')
plt.xlabel('Fruit')
plt.ylabel('Number of Sales')
plt.show()

作者提供的图像

如果我们尝试将相同的数据可视化为折线图,我们会得到上面显示的——一个空图表。折线图通常用于显示随时间变化的趋势。因此,我们需要监控某种‘移动’变量。在这种情况下,可以是每月的销售数据,跨多个不同的月份。

散点图

我们还可以将相同的水果类别映射到一个数字,并将其可视化为散点图。假设我们有 5 个类别及其各自的值。

# Scatter plot
x = [1, 2, 3, 4, 5]
y = [10, 5, 8, 3, 6]
plt.scatter(x, y)
plt.title('Data Points')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

作者提供的图像

正如我们所见,虽然散点图确实显示了不同类别之间的某种差异,并帮助指示它们的表现,但信息仍然没有清晰呈现。

对于这种特定的洞察交付和使用场景,我认为我们都可以同意柱状图可能是最合适的选择。

Infogram 有一篇关于此主题的好文章。

[## 如何为你的数据选择正确的图表

如果你有数据需要可视化,确保使用正确的图表。虽然你的数据可能适用于多个图表……

infogram.com](https://infogram.com/page/choose-the-right-chart-data-visualization?source=post_page-----f00e711b164a--------------------------------)

提示 #2: 有效使用颜色

我不能再强调了——颜色是你在可视化中的最佳朋友。

使用颜色来突出主要的(或值得注意的)见解。

使用颜色来分隔群体。

使用颜色将观众的注意力转移到你希望他们看到的区域。

使用颜色来控制观众的注意力。

为了美观——选择一种与数据和观众相配的令人愉悦的颜色调色板。例如,如果我们展示水果,橙子可能是橙色,香蕉则是黄色。这些小细节是区分好图表和优秀图表的差异。你的观众不应该努力理解图表——而是应该让它对他们说话,告诉他们他们需要知道的一切!

## 选择颜色调色板 - seaborn 0.12.2 文档

由于我们眼睛的工作方式,特定颜色可以通过三个组件来定义。我们通常编程颜色…

seaborn.pydata.org

额外提示——一旦选择了颜色调色板,请保持一致。在所有图表中使用相同的颜色调色板。尤其是在演示过程中。不要让观众困惑。如果在第一个图表中苹果是红色的,就不要在下一个图表中将它们标记为黄色。

回想一下我们之前展示的条形图示例。让我们用一些颜色来装饰它。

import matplotlib.pyplot as plt
import pandas as pd

data = {'apples': 10, 'bananas': 5, 'oranges': 7}

# create a figure with two subplots
fig, axs = plt.subplots(ncols=2, figsize=(10, 4))

# plot the first chart on the left subplot
axs[0].bar(data.keys(), data.values())
axs[0].set_title('Fruit Sales')
axs[0].set_xlabel('Fruit')
axs[0].set_ylabel('Number of Sales')

# Custom color palette
colors = ['#C5283D', '#E9724C', '#FFC857']
# plot the second chart on the right subplot
axs[1].bar(data.keys(), data.values(), color=colors)
axs[1].set_title('Fruit Sales')
axs[1].set_xlabel('Fruit')
axs[1].set_ylabel('Number of Sales')

# adjust the spacing between the subplots
fig.tight_layout()

# show the plot
plt.show()

作者提供的图片

提示 #3: 保持简单

和生活中的大多数事物一样,越简单越好。

如果某些元素或样式对图表没有增加任何价值,就不要包括它们。

同时记住,你的目标是尽可能清晰和高效地向观众展示发现。没有人会在乎你的花哨图形。

额外的内容只会有一个目的:分散观众的注意力。

假设有一个数据集,包含三种不同产品——A、B 和 C 的总销售额。我们想创建一个图表来显示销售趋势随时间的变化。

import matplotlib.pyplot as plt
import numpy as np

# Generate some fake data
months = np.arange(1, 7)
sales_a = np.array([100, 120, 90, 110, 130, 95])
sales_b = np.array([80, 90, 100, 110, 120, 130])
sales_c = np.array([70, 80, 90, 100, 110, 120])

# Create the chart
fig, axs = plt.subplots(3, sharex=True, sharey=True)
axs[0].plot(months, sales_a, color='red')
axs[0].set_title('Product A')
axs[1].plot(months, sales_b, color='green')
axs[1].set_title('Product B')
axs[2].plot(months, sales_c, color='blue')
axs[2].set_title('Product C')
fig.suptitle('Sales by Product')
plt.show()

作者提供的图片

上面的图表完成了任务——但效果相当糟糕。我们有多个图表,每个图表都有自己的刻度、标题和颜色。很难跟踪和比较。

让我们简化一下,好吗?

import matplotlib.pyplot as plt
import numpy as np

# Generate some fake data
months = np.arange(1, 7)
sales_a = np.array([100, 120, 90, 110, 130, 95])
sales_b = np.array([80, 90, 100, 110, 120, 130])
sales_c = np.array([70, 80, 90, 100, 110, 120])

# Create the chart
plt.plot(months, sales_a, color='red', label='Product A')
plt.plot(months, sales_b, color='green', label='Product B')
plt.plot(months, sales_c, color='blue', label='Product C')
plt.title('Sales by Product')
plt.legend()
plt.show()

作者提供的图片

好多了,不是吗?

我们现在可以轻松比较趋势。

当然,这只是一个示例。在生成图表时,请记住,添加到图表中的任何东西都必须具有价值。

提示 #4: 提供背景信息

我不能过分强调这一点——提供背景信息!

令人惊讶的是,我经常看到许多图表没有标题或标签轴。你的观众不是心灵感应者。让他们知道他们在看什么!

在必要时添加标签、标题、图例、数据来源和注释。

这是一个没有任何背景的糟糕图表(左侧)和一个具有背景的优秀图表(右侧)的示例。

作者提供的图像

提示 #5: 使用比例表示

我们有时需要用不同的尺度或范围可视化多个数据。确保我们处理和表示所有变量时使用相同的尺度,并关注有趣的数据范围,是非常重要的。

小心不要误表述你的数据。

例如,考虑下面的图表:

import matplotlib.pyplot as plt
import pandas as pd

# Example data
data = {'apples': 10, 'bananas': 5, 'oranges': 7}

# First chart: bar chart with proportional representation and inconsistent y-axis
plt.subplot(1, 2, 1)
plt.bar(data.keys(), data.values())
plt.ylim(0, 500)
plt.title('Fruit Sales')
plt.xlabel('Fruit')
plt.ylabel('Quantity Sold')

# Second chart: bar chart with proportional representation and consistent y-axis
plt.subplot(1, 2, 2)
plt.bar(data.keys(), data.values())
plt.ylim(0, 12)
plt.title('Fruit Sales')
plt.xlabel('Fruit')
plt.ylabel('Quantity Sold')

# Adjust the spacing between the charts
plt.subplots_adjust(wspace=0.3)

# Display the charts
plt.show()

作者提供的图像

我们非常欣赏两个图表之间的差异。左侧的图表完全失去了比例——使得评估和比较图表变得非常困难。

另一方面,右侧的图表清楚地显示了差异。

提示 #6: 讲述一个故事

我们必须尽量使图表易于理解。

我们还必须尽量使图表尽可能有趣。

优秀的图表传达直接的信息。它们挑选一个有趣的观察或见解并讲述其故事。图表应该用来支持论点。

import matplotlib.pyplot as plt
import pandas as pd

# Example data
year = [2015, 2016, 2017, 2018, 2019, 2020]
sales = [100, 150, 200, 180, 250, 300]

# Line chart with a narrative
plt.plot(year, sales)
plt.title('Sales Growth')
plt.xlabel('Year')
plt.ylabel('Number of Sales')
plt.text(2016.5, 160, 'First year of rapid growth')
plt.text(2018.2, 195, 'Sales decline due to recession')
plt.text(2019.7, 265, 'Sales pick up after the recession')
plt.show()

作者提供的图像

提示 #7: 考虑你的观众

图表应满足观众的需求。

图表应该帮助你的观众更快地得出自己的结论。

不同的观众有不同的需求。

例如,如果我们刚刚训练了预测模型,并且我们将结果呈现给一些高管,我们可能会想要关注财务方面。我们可能会想要突出不同的 KPI 以及该模型如何提高收入。

如果我们向更技术性的观众展示模型,比如其他数据科学家或工程师,我们可能会想要重点关注模型性能方面。我们会想要突出学习曲线或关注评估指标。

观众将影响我们的图表。我们需要使用观众知道和理解的概念和语言。

提示 #8: 使其具有互动性

有时,我们的数据本质上很复杂,而让观众容易理解的唯一方法就是使其具有互动性。

这将允许我们的观众手动探索数据并得出自己的见解。

我们可以通过添加其他互动组件,如工具提示、过滤器和缩放,来帮助他们尽可能参与。

Plotly 是生成互动图表的绝佳工具。

[## Plotly: 低代码数据应用开发]

Dash Enterprise 是构建、扩展和部署 Python 数据应用的首选平台。构建数据应用…

plotly.com](https://plotly.com/?source=post_page-----f00e711b164a--------------------------------)

import plotly.graph_objs as go
import numpy as np

# Generate random data
x = np.random.rand(100)
y = np.random.rand(100)

# Create a Plotly trace object
trace = go.Scatter(
    x = x,
    y = y,
    mode = 'markers'
)

# Create a Plotly layout object
layout = go.Layout(
    title = 'Interactive Scatter Plot',
    xaxis = dict(title = 'X Axis'),
    yaxis = dict(title = 'Y Axis'),
    hovermode = 'closest'
)

# Create a Plotly figure object that combines the trace and layout
fig = go.Figure(data=[trace], layout=layout)

# Display the interactive plot in the Jupyter Notebook
fig.show()

总结

设计美观且有效的数据可视化需要仔细考虑。我们必须记住多种因素,这些因素不仅影响我们展示什么,也影响我们如何展示。我们还必须记住,生成优秀的图表可能需要大量的测试和迭代,以找出最佳的呈现方式。

通过遵循这 8 个数据可视化技巧,你将能够创建更好的可视化图表,有效且清晰地传达你的发现。

你喜欢这篇文章吗?只需每月$5,你就可以成为会员,解锁 Medium 上的无限访问权限。你将直接支持我以及你所有其他喜欢的 Medium 作者。非常感谢!

[## 通过我的推荐链接加入 Medium - David Farrugia

获得对我所有 ⚡优质⚡ 内容的独家访问权限,并在 Medium 上无限制地浏览。通过购买我一杯咖啡来支持我的工作…

david-farrugia.medium.com

想要联系我吗?

我很想听听你对这个话题的看法,或者对人工智能和数据的任何想法。

如果你希望联系我,可以发邮件至 davidfarrugia53@gmail.com

Linkedin

在大学 HPC 集群上训练模型的 9 个技巧

原文:towardsdatascience.com/9-tips-for-training-models-on-your-universitys-hpc-cluster-a703eb87f3d6

如何在资源受限的环境中有效运行和调试代码

Conor O'SullivanTowards Data Science Conor O'Sullivan

·发表于Towards Data Science ·6 分钟阅读·2023 年 3 月 28 日

--

图片由Martijn Baudoin拍摄,刊登在Unsplash

排队作业,等待 24 小时,cuda runtime error: 内存不足

排队作业,等待 24 小时,FileNotFoundError: 没有这样的文件或目录

排队作业,等待 24 小时,RuntimeError: 堆栈期望每个张量……

啊啊啊啊啊!!!

在高性能计算(HPC)集群上调试代码可能非常令人沮丧。更糟糕的是,在大学里你将与其他学生共享资源。作业将被添加到队列中。你可能要等待几个小时才能知道代码是否有错误。

最近我在大学的 HPC 上训练模型。我学到了一些东西(是艰难的方式)。我想分享这些技巧,希望能让你的体验更顺利。我会保持通用,以便你可以将它们应用到任何系统。

尽可能在个人机器上进行开发

HPC 集群有一个目标——处理数据。没有华丽的 IDE,没有调试器,也没有副驾驶。你真的想用 vim 编写整个项目吗?

为了避免发疯(把那些留给你的论文吧),你应该在自己的机器上开发代码。用小样本训练模型至少1 个周期。包含测试以确保数据正确加载并且所有结果都已保存。训练模型 50 个周期却发现忘记保存最佳结果是毫无乐趣的(是的,我确实做过这个)。

保持代码简单

你在代码中包含的任何额外步骤都会增加运行失败的可能性。你应该只运行需要大量计算能力的进程。例如,模型训练完成后,你会想要对其进行评估。这可以在本地机器上完成。如果你的测试集足够小,甚至 CPU 也可以使用。

不要硬编码任何路径或变量

当将代码从本地机器移动到 HPC 时,某些事情不可避免地需要更改。例如,加载数据和保存结果的文件路径。我是在 Mac 上开发的,但 HPC 使用的是 Linux 操作系统。这意味着我需要将设备从“mps”更改为“cuda”。

图 1:待更新变量示例(来源:作者)

不要硬编码任何需要更改的内容。使用变量并在脚本顶部定义它们。这使得它们易于更改,并且你不会忘记任何东西。相信我!你不想用 vim 滚动查看代码行。

增加批量大小

在上面的图 1中,你可以看到我包含的一个变量是batch_size。这是每次迭代中加载和用于更新模型参数的样本数量。较大的批量大小意味着你可以在 GPU 上并行处理更多样本,从而加快训练时间。

在本地机器上增加这一点很快会导致“cuda runtime error: out of memory”错误。相比之下,HPC 可以处理更大的批量大小。然而,重要的是不要增加过多,因为这可能会对模型准确性产生负面影响。我只是将批量大小从 32 加倍到 64。

对任何实验使用系统参数

尽可能避免在 HPC 上编辑代码。同时,为了训练最佳模型,你需要进行实验。为了解决这个问题,使用系统参数。如图 2所示,这些参数允许你在命令行中更新脚本中的变量。

图 2:系统参数示例(来源:作者)

第一个变量允许我更新最终模型的保存路径(model_name)。其他变量允许我以各种方式采样、缩放清理数据(见图 3)。你甚至可以更新模型架构。例如,通过传递一个列表[x,y,z]来定义神经网络隐藏层中的节点数量。

图 3:用 4 个数据清理实验训练 U-Net(来源:作者)

包含一个示例参数

sample 系统参数特别有用。我上面包含的这个是一个二进制标志。如果设置为 true,建模代码将使用 1000 个样本的子集运行。你也可以传递实际的样本数量作为整数。

我通常会在两个作业之间安排紧接着的运行——一个是采样数据集运行,另一个是完整数据集运行。采样运行通常会在几分钟内完成(除非有人占用了所有的 GPU!!)。这有助于指出那些烦人的错误。如果出现问题,我有机会在一天结束前停止完整数据集的运行,进行修正,然后重新启动。

详尽说明

不,我不是在谈论你那个主修金融的朋友。你的代码应该尽可能地告诉你更多信息。这将帮助修复任何出现的错误。使用那些打印语句!我发现每次运行时打印的一些有用信息包括:

  • 系统设备(即 cuda 或 CPU)

  • 数据集的长度、训练集和验证集

  • 数据在任何转换前后的样本

  • 每个 epoch 和 batch 的训练损失和验证损失

  • 模型架构

  • 任何系统参数的值

你永远不知道会出什么问题。对于机器学习来说,逻辑错误可能很难被识别。你的代码可能运行得很好,但产生的模型却是完全无用的。拥有一个过程记录将帮助你追溯错误的来源。如果你打算进行多次实验,这一点尤其重要。

使用 diff 比较工具

事情会出错的。非常糟糕。我打破了自己的规则,在 HPC 上对脚本进行了几处修改。好吧,我做了很多修改。我失去了对自己所做更改的追踪,代码无法正常运行。在这种情况下,diff 比较工具就派上了用场。

它会逐行比较一个文档中的文本与另一个文档中的文本。我用它来比较 HPC 上的脚本与我本地机器上的一个脚本。这指出了我所做的更改,我可以立即识别出问题。

图 4:指出 HPC 和本地脚本中的差异(来源:diff 比较工具

对你的研究保持现实

目前的提示还缺乏细节。你可以做很多事情来加快特定模型包(例如 XGBoost)的训练速度更好地跟踪你的数据和模型也可以帮助避免错误和混乱。但实际上,你能做的也有限。我的最终建议是对你在可用资源下能实现的目标保持现实。

在 LLM 时代,这可能会令人沮丧。所有主要的突破似乎都是通过增加计算能力来实现的。你必须考虑到,一个像 GPT-3 这样大小的模型训练成本估计为 45 万美元。这与一些部门的整个预算相当!你根本无法竞争。

然而,您仍然可以做出有价值的贡献。微调模型不需要那么多资源。收集和标注数据往往比在基准测试中追求 SOTA 更为重要。每个应用机器学习的子领域都会带来自己的挑战。通常,资源的匮乏会促使我们对这些挑战提出更具创新性的解决方案。所以要感谢那些漫长的工作队列……

哦,我在自欺欺人!!有谁愿意买给我一块 GPU 吗?

希望您喜欢这篇文章!您可以通过成为我的 推荐会员 😃 来支持我

[## 通过我的推荐链接加入 Medium — Conor O’Sullivan]

作为 Medium 会员,您的会员费的一部分会分配给您阅读的作者,您可以全面访问每个故事……

conorosullyds.medium.com

| Twitter | YouTube | Newsletter — 免费注册获取 Python SHAP 课程

参考文献

Michael Galarnyk 如何加速 XGBoost 模型训练 medium.com/towards-data-science/how-to-speed-up-xgboost-model-training-fcf4dc5dbe5f

Leonie Monigatti MLOps 简介:数据和模型版本管理 medium.com/@iamleonie/intro-to-mlops-data-and-model-versioning-fa623c220966

Kevin Shen 批量大小对训练动态的影响 medium.com/mini-distill/effect-of-batch-size-on-training-dynamics-21c14f7a716e

91% 的机器学习模型随着时间的推移会退化

原文:towardsdatascience.com/91-of-ml-models-degrade-in-time-cfd467905615?source=collection_archive---------10-----------------------#2023-04-11

随着时间的推移和数据分布的变化,机器学习模型的性能会退化。

Santiago VíquezTowards Data Science Santiago Víquez

·

关注 发表在 Towards Data Science ·9 min read·2023 年 4 月 11 日

--

模型老化图表展示了机器学习模型随着时间推移的性能退化情况。图片摘自原始论文,由作者注释。

最近来自麻省理工学院、哈佛大学、蒙特雷大学和剑桥大学的研究显示,91% 的机器学习模型随着时间推移会退化。这是同类研究中的首个之一,研究人员关注于机器学习模型在部署后的行为及其性能如何随未见数据的发展而变化。

“虽然已经对各种类型和时间数据漂移的标记进行了大量研究,但尚未有关于模型自身如何应对这些漂移的全面研究。”

本博客文章将回顾研究的关键部分,突出其结果,并强调这些结果的重要性,特别是对机器学习行业的意义。

介绍

如果你以前接触过像协变量偏移或概念漂移这样的概念,你可能知道生产数据分布的变化可能会影响模型的性能。这种现象是维护生产环境下机器学习模型的一大挑战。

根据定义,机器学习模型依赖于其训练的数据,这意味着如果生产数据的分布开始变化,模型可能不再像以前那样表现良好。随着时间的推移,模型的性能可能会越来越差。作者称这种现象为“AI 老化。” 我喜欢称之为模型性能退化,具体的性能下降幅度可能让我们将其视为模型失败。

为了更好地理解这种现象,作者开发了一个识别时间性模型退化的框架。他们将该框架应用于来自四个行业的 32 个数据集,使用四种标准机器学习模型,并调查了在数据的最小漂移下,时间性模型退化如何发展。

模型和数据

为了避免任何模型偏差,作者选择了四种不同的标准机器学习方法(线性回归、随机森林回归、XGBoost 和多层感知器神经网络)。这些方法代表了从数据中学习的不同数学方法。通过选择不同的模型类型,他们能够调查不同模型在相同数据上的老化方式的相似性和差异。

同样,为了避免领域偏差,他们选择了来自四个行业(医疗保健、天气、机场交通和金融)的 32 个数据集。

另一个关键决定是,他们只调查了初始性能良好的模型-数据集对。这一决定至关重要,因为研究一个初始适配差的模型的退化是不值得的。

时间退化实验中使用的原始数据示例。时间轴在横轴上,每个数据集的目标变量在纵轴上。当每天收集了多个数据点时,它们会用背景颜色和移动的日均曲线来显示。突出标题的颜色将在博客文章中用于轻松识别每个数据集的行业。图片来自原始论文,由作者标注。

提议的框架

为了识别时间性模型性能退化,作者设计了一个模拟典型生产机器学习模型的框架,并按照该框架进行了多个数据集-模型实验。

对于每个实验,他们做了四件事:

  • 随机选择一年的历史数据作为训练数据。

  • 选择一个机器学习模型。

  • 随机选择一个未来的日期时间点来测试模型。

  • 计算模型性能的变化

为了更好地理解框架,我们需要几个定义。训练数据中最新的点定义为t_0。在t_0和测试模型的未来时间点之间的天数定义为dT,它象征着模型的年龄。

例如,一个天气预报模型使用 2022 年 1 月 1 日至 12 月 31 日的数据进行训练。然后在 2023 年 2 月 1 日,我们要求它进行天气预报。

在这种情况下

  • t_0 = 2022 年 12 月 31 日,因为这是训练数据中最新的点。

  • dT = 32 天(从 12 月 31 日到 2 月 1 日的天数)。这是模型的年龄。

下图总结了他们如何执行每次“历史-未来”模拟。我们添加了注释以便于理解。

AI 时间降解实验的图示。图片取自原始论文,由作者标注。

为了量化模型性能的变化,他们测量了在时间t_0时的均方误差(MSE)为MSE(t_0),以及在模型评估时的MSE(t_1)

由于MSE(t_0)应该较低(每个模型在接近训练日期时表现良好)。可以将相对性能误差测量为MSE(t_0)MSE(t_1)之间的比率。

E_rel = MSE(t_1)/MSE(t_0)

研究人员为每个数据集-模型对进行了 20,000 次这种类型的实验!其中t_0dT是从均匀分布中随机抽取的。

在进行所有这些实验之后,他们为每个数据集-模型对报告了一个老化模型图表。该图表包含 20,000 个紫色点,每个点代表训练后dT天获得的相对性能误差E_rel

财务数据集和神经网络模型的模型老化图表。每个小点代表一次单独的时间降解实验结果。图片取自原始论文,由作者标注。

图表总结了随着模型年龄的增加,模型性能的变化。关键要点:

  1. 误差随时间增加: 随着时间的推移,模型的性能越来越差。这可能是由于模型的任何特征存在漂移或由于概念漂移造成的。

  2. 误差变异性随时间增加: 随着模型的老化,最佳和最差情况之间的差距增加。当一个机器学习模型具有高误差变异性时,意味着它有时表现良好,有时表现不佳。模型性能不仅仅是退化,还存在不稳定行为。

合理较低的中位数模型误差可能仍会产生准确模型性能的错觉,而实际结果变得越来越不确定。

主要降解模式

在对所有 4(模型)x 32(数据集)= 128(模型、数据集)对进行实验后,91%的情况下观察到了时间上的模型性能下降。接下来,我们将探讨四种最常见的性能下降模式及其对 ML 模型实现的影响。

渐进或无降级

尽管在下面的两个示例中没有观察到强烈的性能下降,但这些结果仍然提出了挑战。查看原始的患者和天气数据集,我们可以看到患者数据中的延迟变量有很多异常值。相比之下,天气数据中的温度变量则有季节性变化。但即便在这些目标变量的行为下,两种模型似乎随着时间的推移表现准确。

渐进的 ML 模型降级模式,相对模型错误随时间的增加不超过线性。图片来源于原始论文,由作者注释。

作者声称,这些及类似结果表明,单凭数据漂移无法解释模型失败或触发模型质量检查和再训练。

我们在实践中也观察到了这一点。数据漂移并不一定意味着模型性能下降。这就是为什么在我们的ML 监控工作流程中,我们关注于性能监控,仅使用数据漂移检测工具来调查性能下降问题的合理解释,因为仅凭数据漂移不应触发模型质量检查。

爆炸性降级

模型性能下降也可能非常突然。查看下图,我们可以看到两种模型在第一年表现良好。但在某个时刻,它们开始以爆炸性的速度下降。作者声称,这些性能下降不能仅通过数据中的特定漂移来解释。

爆炸性 ML 模型老化模式。图片来源于原始论文,由作者注释。

比较两个使用相同数据集但不同 ML 模型的模型老化图。左侧是爆炸性降级模式,而右侧几乎没有降级。两种模型最初表现良好,但神经网络似乎比线性回归(标记为 RV 模型)更快地降级。

爆炸性与无降级对比。图片来源于原始论文,由作者注释。

鉴于此及类似结果,作者总结认为时间性模型质量取决于 ML 模型的选择及其在特定数据集上的稳定性

在实践中,我们可以通过持续监控估计的模型性能来应对这种现象。这使我们能够在发现爆炸性降级之前解决性能问题。

错误变异性增加

尽管黄色(第 25 百分位数)和黑色(中位数)线保持在相对较低的误差水平,但它们与红色线(第 75 百分位数)之间的差距随着时间显著增加。如前所述,这可能会造成准确模型性能的错觉,而实际模型结果却变得越来越不确定。

增加不可预测的人工智能模型老化模式。图像取自原始论文,由作者标注。

单凭数据或模型本身都无法保证一致的预测质量。相反,时间模型的质量取决于在特定时间对特定数据应用的特定模型的稳定性。

潜在解决方案

一旦我们找到了模型老化问题的根本原因,我们可以寻找最佳的解决技术。适当的解决方案依赖于具体情境,因此没有一种简单的修复方法适用于所有问题。

每当我们看到模型性能下降时,我们应该调查问题并理解其原因。自动修复几乎不可能针对每种情况进行概括,因为多个原因可能导致退化问题。

在论文中,作者提出了一个可能的解决方案来应对时间退化问题。该方案集中于机器学习模型的再训练,并假设我们可以获得新的标注数据,数据质量没有问题,并且不存在概念漂移。为了使这个解决方案在实际中可行,他们提到需要以下条件:

1. 当你的模型需要再训练时发出警报。

当模型的性能出现退化时发出警报并非易事。需要访问最新的真实数据或能够估计模型的性能。像 DLE 和 CBPE 这样的解决方案来自 NannyML 可以提供帮助。例如,DLE(直接预测估计)和 CBPE(基于置信度的性能估计)使用概率方法来估计模型的性能,即使目标缺失时也能进行估计。他们监控估计的性能,并在模型退化时发出警报。

图表取自 NannyML

2. 开发一个高效且稳健的自动模型再训练机制。

如果我们知道没有数据质量问题或概念漂移,频繁使用最新标注数据再训练机器学习模型可能有帮助。然而,这可能会带来新的挑战,例如模型收敛不足、训练参数的次优变化以及 “灾难性遗忘”,即人工神经网络在学习新信息时突然忘记以前学习的信息的倾向

3. 持续访问最新的真实数据。

最新的真实数据将允许我们重新训练机器学习模型并计算实际性能。问题在于,实际上,真实数据往往会延迟,或者获取新标记数据既昂贵又耗时。

当重新训练非常昂贵时,一个潜在的解决方案是拥有一个模型目录,然后利用估计的性能选择预期表现最佳的模型。这可以解决在相同数据集上不同模型老化速度不同的问题。

行业内其他流行的解决方案包括将模型恢复到之前的检查点,修复下游问题,或改变业务流程。要了解每种解决方案的最佳应用时机,请查看我们之前关于如何应对数据分布变化的博客文章。

结论

Vela 等人的研究显示,尽管机器学习模型在部署时可能达到高准确率,但其性能不会保持静态。即使在相同数据集上训练,不同的机器学习模型也会以不同的速度老化。另一个相关的观察是,并非所有的时间漂移都会导致性能下降。因此,模型的选择及其稳定性也成为处理性能时间退化的最关键因素之一。

这些结果为为什么监控解决方案对机器学习行业至关重要提供了理论支持。此外,它显示了机器学习模型性能容易退化。这就是为什么每个生产中的机器学习模型都必须进行监控,否则模型可能在没有警示的情况下失败。

参考文献

Vela, D., Sharp, A., Zhang, R., et al. 时间质量退化在 AI 模型中。Sci Rep 12, 11654 (2022). doi.org/10.1038/s41598-022-15245-z

我在TwitterLinkedIn上不断撰写关于数据科学和机器学习的内容。如果你想和我一起在公开场合继续学习,请关注我 😃

评估检索增强生成(RAG)的 3 步法

原文:towardsdatascience.com/a-3-step-approach-to-evaluate-a-retrieval-augmented-generation-rag-5acf2aba86de

停止随机选择你的 RAG 参数

Ahmed BesbesTowards Data Science Ahmed Besbes

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

--

照片由Adi Goldstein拍摄,来源于Unsplash

调整你的 RAG 以获得最佳性能需要时间,因为这取决于各种相互关联的参数:块大小、重叠、检索的前 K 个文档、嵌入模型、LLM 等。

最佳组合通常依赖于你的数据和使用案例:你不能仅仅套用上一个项目中的设置来期望得到相同的结果。

大多数人没有妥善解决这个问题,而是几乎随机地选择参数。虽然有些人对此方法感到舒适,但我决定用数值方法来解决这个问题。

这就是评估你的 RAG 的地方。

这篇文章中,我将展示一个快速的 3 步法,你可以用来高效、快速地评估你的 RAG 在两个任务中的表现。

  1. 检索

  2. 生成

通过掌握这个评估流程,你可以进行迭代,执行多次实验,用指标进行比较,并且希望找到最佳配置

让我们看看这个如何运作 👇。

附注:在每一节中,都提供了代码片段以帮助你开始实现这些想法。

如果你对提高构建 ML 系统生产力的实用技巧感兴趣,可以随时订阅我的通讯: The Tech Buffet。

我每周发送编程和系统设计的见解,以帮助你更快地发布 AI 产品。

1 — 创建一个合成数据集

评估 LLM 通常需要手动标注测试集。这需要时间、领域专业知识,并且容易出错。

希望 LLM 能帮助我们完成这项任务。

从你的数据中抽取 N 个块。对于每个块,指示 LLM 生成 K 个问题和答案的元组。

生成完成后,你将获得一个包含 N*K 元组的数据集,每个元组都有(问题,答案,上下文)。

附注:这里的上下文是原始块及其元数据

在以下示例中,我们将考虑一个提到艾萨克·牛顿的段落。

艾萨克·牛顿因其关于引力定律的理论而闻名,但他的《自然哲学的数学原理》(1686 年)

通过其三大运动定律极大地影响了欧洲的启蒙时代。生于 1643 年

在英格兰的伍尔索普,艾萨克·牛顿爵士开始发展他的理论

关于光、微积分和天体力学,同时在剑桥大学休假期间。

我们将指示 LLM 从中生成三对问题和答案。

图片由作者提供

这可以使用这个提示完成。(改编自我的一个项目)

Create exactly {num_questions} questions using the context and make 
sure each question doesn’t reference
terms like "this study", "this research", or anything that’s 
not available to the reader.
End each question with a ‘?’ character and then in a newline
write the answer to that question using only 
the context provided.

Separate each question/answer pair by "XXX"

Each question must start with "question:".

Each answer must start with "answer:".

CONTEXT = {context}

我们将进一步了解这个合成数据集的局限性,但现在,它是评估我们实验的一个快速解决方案。

代码 💻

在接下来的部分中,我将展示如何使用 LangChain 创建你的合成评估数据集。

我们首先定义提示和 LLM:我正在使用 GCP,所以我将使用 VertexAI。

from random import sample
from langchain.llms import VertexAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain

SYNTHETIC_DATASET_SIZE = 70
NUM_QUESTIONS_TO_GENERATE = 3
sampled_chunks = sample(splits, k=SYNTHETIC_DATASET_SIZE)

prompt = """
Create exactly {num_questions} questions using the context and make sure each question doesn't reference
terms like "this study", "this research", or anything that's not available to the reader.

End each question with a '?' character and then in a newline write the answer to that question using only 
the context provided.
Separate each question/answer pair by "XXX"
Each question must start with "question:".
Each answer must start with "answer:".
CONTEXT = {context}
"""

llm = VertexAI(
    model_name="text-bison",
    max_output_tokens=256,
    temperature=0,
    top_p=0.95,
    top_k=40,
    verbose=True,
)
prompt_template = PromptTemplate(
    template=prompt,
    input_variables=[
        "num_questions",
        "context",
    ],
)
chain = LLMChain(llm=llm, prompt=prompt_template)

然后,我们从 BQ 加载一些数据并将其拆分成块。块将是生成问题和答案所需的上下文。

from langchain.document_loaders.bigquery import BigQueryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

def load_data_from_bq(query, content_cols, metadata_cols):
    loader = BigQueryLoader(
        query=query, page_content_columns=content_cols, metadata_columns=metadata_cols
    )
    return loader.load()

query = ... # the SQL query to 
content_cols = ... # the content columns of your table
metadata_cols = ... # the metadata columns of your

loaded_data = load_data_from_bq(
    query,
    content_cols=content_cols,
    metadata_cols=metadata,
)

CHUNK_SIZE = 700
CHUNK_OVERLAP = 100

text_splitter = RecursiveCharacterTextSplitter(
    separators=["."],
    chunk_size=CHUNK_SIZE,
    chunk_overlap=CHUNK_OVERLAP,
)

splits = text_splitter.split_documents(loaded_data)

一旦数据加载并拆分完毕,你需要从中抽取一些块以生成问题/答案对。

这在以下循环中完成。

import pandas as pd

SYNTHETIC_DATASET_SIZE = 70
NUM_QUESTIONS_TO_GENERATE = 3
sampled_chunks = sample(splits, k=SYNTHETIC_DATASET_SIZE)

synthetic_dataset = []
for sampled_chunk in tqdm(sampled_chunks):
    prediction = chain.invoke(
        {
            "num_questions": NUM_QUESTIONS_TO_GENERATE,
            "context": sampled_chunk.page_content,
        }
    )
    output = prediction["text"]

    try:
        questions_and_answers = parse_output(output)

        for question_and_answer in questions_and_answers:
            synthetic_dataset.append(
                {
                    **question_and_answer,
                    "context": sampled_chunk.page_content,
                    "source": sampled_chunk.metadata["source"],
                }
            )
    except:
        pass

synthetic_dataset_df = pd.DataFrame(synthetic_dataset)

2 — 对每个合成问题运行你的 RAG

一旦合成数据集构建完成,你可以使用你的 RAG 对每个问题进行预测。

这将生成基于一组检索来源的答案。

对于每个问题,请确保你提取了检索到的文档:这些文档将用于进行评估。

图片由作者提供

代码 💻

在接下来的部分中,我们将看到如何构建 RAG 并对评估数据进行预测。

我们首先需要创建一个向量数据库来索引这些块。

from langchain.embeddings import VertexAIEmbeddings
from langchain.vectorstores import Chroma

db = Chroma.from_documents(
    documents=splits,
    embedding=VertexAIEmbeddings(),
    persist_directory="./db",
)

LangChain 中默认的 RAG 实现使用了一个特定的提示模板。在我们的案例中,我们将稍微修改它以防止产生幻觉。

RAG_TEMPLATE = """Use the following pieces of context to answer the question at the end. 
If you don't know the answer, just say that you don't know, don't try to make up an answer. 
{context}
Question: {question}
Helpful Answer:"""

然后,我们将我们的 RAG 实现包装在一个 Python 类中。

class RAG:
    def __init__(self, vectorstore, rag_template=RAG_TEMPLATE):
        self.vectorstore = vectorstore
        self.rag_prompt = PromptTemplate.from_template(rag_template)
        self.chain = LLMChain(llm=llm, prompt=self.rag_prompt)

    def _format_context(self, docs):
        context = [doc.page_content for doc, score in docs]
        context = "\n---\n".join(context)
        return context

    def _format_source_documents(self, docs):
        source_documents = []
        for doc, score in docs:
            doc.metadata["score"] = score
            source_documents.append(doc)
        return source_documents

    def predict(self, question, k=4, score_threshold=0.6):
        relevant_documents = self.vectorstore.similarity_search_with_relevance_scores(
            query=question,
            k=k,
            score_threshold=score_threshold,
        )
        source_documents = self._format_source_documents(relevant_documents)
        context = self._format_context(relevant_documents)
        answer = self.chain.predict(question=question, context=context)
        output = {
            "question": question,
            "answer": answer,
            "source_documents": source_documents,
        }
        return output

# Create the RAG:

rag = RAG(vectorstore=db)

一旦 RAG 构建和初始化完成,你可以进行预测:

evaluations = []

for i, row in tqdm(synthetic_dataset_df.iterrows(), total=len(synthetic_dataset_df)):
    question = row["question"]
    ground_truth_answer = row["answer"]
    ground_truth_context = row["context"]
    ground_truth_source = row["source"]
    evaluation = {
        "question": question,
        "ground_truth_answer": ground_truth_answer,
        "ground_truth_source": ground_truth_source,
    }

    prediction = rag.predict(question)
    predicted_answer = prediction["answer"]
    predicted_contexts = [
        source_document.page_content
        for source_document in prediction["source_documents"]
    ]
    predicted_sources = [
        source_document.metadata["source"]
        for source_document in prediction["source_documents"]
    ]

    evaluation["predicted_answer"] = predicted_answer
    evaluation["predicted_contexts"] = predicted_contexts
    evaluation["predicted_sources"] = predicted_sources
    evaluation["relevance_scores"] = [
        source_document.metadata["score"]
        for source_document in prediction["source_documents"]
    ]

    evaluations.append(evaluation)

df_evaluations = pd.DataFrame(evaluations)

完成后,你将大致得到以下结果:

截图由作者提供

3 — 计算两个评估指标

现在,你可以对你为每个问题做出的预测计算两个评估指标。

  1. 一个检索分数用于评估检索到的文档的相关性

    这个分数可以是二进制的(每个预测为 1/0),并且表示每个问题的真实来源是否在检索到的来源列表中。你可以把这个分数看作是召回率

  2. 一个质量评分用于评估生成的答案,给定问题和真实答案。再次地,可以使用 LLM 在此任务中提供 5 分评价。

这是我用来要求 LLM 评分生成答案的提示。

Your job is to rate the quality of a generated answer
given a query  and a reference answer.

QUERY = {query}

GENERATED ANSWER = {generated_answer}

REFERENCE ANSWER = {reference_answer}

Your score has to be between 1 and 5.
You must return your response in a line with only the score.
Do not return answers in any other format.
On a separate line provide your reasoning for the score as well.

在获得每个问题的分数后,对数据集进行平均以获得最终的两个指标。

作者图片

代码 💻

在这一部分中,我们从df_evaluations数据框开始并计算不同的指标。

1 — 检索分数

我们可以通过计算 top_k 指标开始,这些指标表明真实来源是否在前 k 个预测来源中。

df_evaluations["top_1"] = df_evaluations.apply(
    lambda row: row["ground_truth_source"] in row["predicted_sources"][:1], axis=1
)
df_evaluations["top_2"] = df_evaluations.apply(
    lambda row: row["ground_truth_source"] in row["predicted_sources"][:2], axis=1
)
df_evaluations["top_3"] = df_evaluations.apply(
    lambda row: row["ground_truth_source"] in row["predicted_sources"][:3], axis=1
)
df_evaluations["top_4"] = df_evaluations.apply(
    lambda row: row["ground_truth_source"] in row["predicted_sources"][:4], axis=1
)

对所有文档进行平均,将给我们每个 k 的检索分数。

作者截图

如何解读这些分数:

示例:真实来源在前四个检索来源中的概率为 0.37。

2 — 生成分数

为了评估生成答案的质量,我们将使用一个 LLM,它将提供其评分和推理。

EVAL_TEMPLATE = """Your job is to rate the quality of a generated answer
given a query  and a reference answer.

QUERY = {query}

GENERATED ANSWER = {generated_answer}

REFERENCE ANSWER = {reference_answer}

Your score has to be between 1 and 5.
You must return your response in a line with only the score.
Do not return answers in any other format.
On a separate line provide your reasoning for the score as well."""

prompt_template_eval = PromptTemplate.from_template(EVAL_TEMPLATE)
llm_eval_chain = LLMChain(llm=llm, prompt=prompt_template_eval)

在这个函数中,我们指导这个 LLM 并获取其评分和推理:

  1. 查询

  2. 预测的答案

  3. 参考答案

def evaluate_answer(query, generated_answer, reference_answer):
    answer = llm_eval_chain.invoke(
        {
            "query": query,
            "generated_answer": generated_answer,
            "reference_answer": reference_answer,
        }
    )
    answer = answer["text"]

    try:
        rating, reasoning = answer.split("\n")
        rating = rating.strip()
        reasoning = reasoning.strip()

        evaluation = {
            "rating": rating,
            "reasoning": reasoning,
        }
        return evaluation
    except:
        return {
            "rating": None,
            "reasoning": None,
        }

然后,我们对评估数据运行这个函数:

answer_quality_evaluations = []

for i, row in tqdm(df_evaluations.iterrows(), total=len(df_evaluations)):
    evaluation = evaluate_answer(
        row["question"],
        row["predicted_answer"],
        row["ground_truth_answer"],
    )
    answer_quality_evaluations.append(evaluation)

生成评分后,我们得到的分布如下:

作者截图

根据评分过滤,以下是一些 RAG 未能生成答案的问题:

作者截图

这里是一些其他成功的例子。

作者截图

分析这些数据有助于诊断和解决 RAG 出现的错误。

这种评估方法的局限性

在为制药行业构建多个 RAG 时,我应用了这种评估方法。

虽然它快速提供了一个端到端的管道并避免了手动标记数据,但我注意到它有两个主要问题:

  • 一些生成的问题过于具体,对于没有阅读上下文的人来说毫无意义。因此,它们对评估指标造成了很大的负面影响,因为它们是不可能回答的。

    例如:“这项研究讨论的主要问题是什么?”

  • 另一方面,一些其他问题过于简单,或者只是原始片段的简单改述。

结论

如果你想了解更多关于这种评估方法(也称为冷启动),可以查看这个指南

如果你是 RAG 的新手并想深入了解,你也可以查看我之前的一些帖子以获取更多信息。

你知道其他在构建 RAG 时被证明有效的评估方法吗?请告诉我。

如果你也在研究 RAG 的最佳性能,我也很愿意讨论这个话题。

今天就到这里。下次再见!👋

使用策略梯度强化学习进行 A/B 优化

原文:towardsdatascience.com/a-b-optimization-with-policy-gradient-reinforcement-learning-b4a3527f849?source=collection_archive---------13-----------------------#2023-05-23

关于策略梯度方法的逐步视觉解释

Dr. Bernd EbenhochTowards Data Science Dr. Bernd Ebenhoch

·

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

--

明智选择!(图像由作者借助 Stable Diffusion Online 创建)

在这篇文章中,我们将探讨如何将策略梯度强化学习应用于 A/B 优化。这是一个简单的演示,以观察策略梯度方法,在这里我们将深入理解其基础机制,并逐步可视化学习过程。

介绍

强化学习是机器学习中的一个基本概念,与监督学习、自监督学习和无监督学习一样。在强化学习中,智能体试图在环境中找到最佳的一组动作以最大化奖励。强化学习因其与神经网络结合后作为高度灵活的智能体能够击败围棋和象棋的顶级玩家而广为人知。

用作智能体的神经网络通过最大化获得的奖励逐步优化策略。已经开发出几种更新神经网络参数的策略,如策略梯度、Q 学习或演员评论学习。策略梯度方法最接近于反向传播,这在神经网络的监督学习和自监督学习中常常使用。然而,在强化学习中,我们不会像在监督学习中那样直接评估每个动作,而是尝试最大化总奖励,并让神经网络决定采取的具体动作。这些动作从概率分布中选择,这为探索提供了高度的灵活性。在优化初期,动作是随机选择的,智能体探索不同的策略。随着时间的推移,一些动作证明比其他动作更有用,概率分布会收敛到明确的决策上。与其他强化学习方法不同,用户不需要控制探索与利用之间的平衡,最佳平衡由梯度策略方法本身找到。

通常,为了最大化奖励,最佳策略是通过一系列动作实现的,每个动作都会导致环境的新状态。然而,梯度策略方法也可以用来找到统计上奖励最高的最佳动作。这种情况通常出现在进行 A/B 优化时,这是一种选择两个选项中较好者的常见技术。例如,在营销中,A/B 测试用于选择能够带来更高销售的广告方案。你更愿意点击哪个广告?选项 A:“充分利用你的数据:**我是一名专业的数据科学家——我可以帮助你分析数据”还是选项 B:“数据处理遇到困难?**专业数据分析师有空帮助你自动化数据分析”?

两种广告创意选项。你更愿意点击哪一个?(图片由作者创作)

A/B 优化的难点在于点击率是变量。例如,在网站上看到广告后,每个用户可能有不同的偏好、处于不同的心情,因此反应也会不同。由于这种变异性,我们需要统计技术来选择更好的广告方案。比较 A 和 B 选项的常见方法是假设检验,如 t 检验。要进行 t 检验,必须展示广告的两个潜在版本一段时间,以从用户那里收集点击率。为了获得对首选广告方案的显著评估,需要相当长时间的探索,这会带来潜在的收入损失,因为在探索期间,好的和差的广告会随机展示得一样频繁。尽快通过更频繁地展示更好的广告来最大化点击率是有利的。通过使用梯度策略方法进行 A/B 优化,代理首先会随机探索 A 和 B 两个变体,但对点击率更高的广告给予更高的奖励,因此代理会迅速学会更频繁地向用户展示更好的广告,从而最大化点击率和收入。

示例

在我们的例子中,我们有两个广告创意选项,假设选项 A 的点击概率为 30%,选项 B 的点击概率为 40%。我们运行一个包含 1000 次广告展示的广告活动。如果我们仅进行探索,并且两个选项展示的频率相同,我们可以期望平均点击率为 35%,总点击数为 350 次。如果我们知道 B 的点击率会更高,我们只会展示 B 并获得平均 400 次点击。然而,如果我们运气不好,选择只展示 A,我们将获得平均仅 300 次点击。使用政策梯度方法,我们可以实现平均 391 次点击,这清楚地表明,快速应用学习到的策略可以获得几乎与首次选择更好的 B 选项一样多的点击数。

如何运作——逐步说明

我们使用 TensorFlow 库在一个小型神经网络上运行 A/B 优化,采用梯度策略方法。首先,我们需要进行一些导入。

import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf

神经网络仅包含一层单个神经元,该神经元决定展示哪个广告。由于我们没有关于用户偏好、位置、时间或其他任何信息,因此决策基于对神经网络的零输入,我们不需要大神经网络所实现的非线性。训练是通过调整该神经元的偏置来实现的。

model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Dense(1, activation="sigmoid", input_shape=(1,)))
model.summary()

使用神经网络的函数来选择动作,显示选项 A 或选项 B。该函数装饰为 tf.function(),这会创建一个静态计算图,使其运行速度比在急切模式下快得多。使用 TensorFlow 的 GradientTape 函数,我们在广告选择过程中收集梯度。每当用户进入网站时,神经网络会产生一个输出,这个输出被视为选择展示给用户的广告变体 A 或变体 B 的概率。由于我们只有一个带有 sigmoid 激活的神经元,输出是介于 0 和 1 之间的单一数字。如果输出为 0.5,则广告 B 和广告 A 的展示机会各为 50%。如果输出为 0.8,则广告 B 的展示机会为 80%,广告 A 的展示机会为 20%。通过将神经网络的输出与介于 0 和 1 之间的均匀分布的随机数进行比较来选择动作。如果随机数小于输出,则动作为 True(1),选择广告 B;如果随机数大于输出,则动作为 False(0),选择广告 A。损失值使用 binary_crossentropy_loss 来衡量神经网络输出与所选动作之间的差异。然后我们创建损失相对于模型参数的梯度。

@tf.function()
def action_selection(model):
    with tf.GradientTape() as tape:
        output = model(np.array([[0.0]]))  # [0 ... 1]
        action = (tf.random.uniform((1, 1)) < output)  # [0 or 1]

        loss = tf.reduce_mean(tf.keras.losses.binary_crossentropy(action, output))

    grads = tape.gradient(loss, model.trainable_variables)
    return output, action, loss, grads

我们在 1000 次广告展示中进行训练。在每一步中,广告被展示一次,新的用户有机会点击广告。为了评估学习过程,我们计算这一阶段的点击总数。学习率定义为 0.5。我们将稍后讨论学习率对点击总数的影响。

STEPS = 1000
LR = 0.5

现在,让我们运行广告活动。神经网络将随着时间的推移改进其预测。通过强化学习,训练和应用同时进行。在实践中,所选广告现在会显示在网站上,我们需要等待并查看用户是否点击了广告或离开了网站而没有点击。在代码中,我们只是模拟用户是否点击。正如上面所述,广告 A 有 30%的点击机会,而广告 B 有 40%的点击机会。点击可以直接作为奖励来训练神经网络。奖励用于修改梯度。如果用户点击了广告,该动作的梯度保持不变;但如果用户没有点击广告,梯度则会被反转。最后,梯度下降通过为神经网络分配新的权重和偏置值来更新神经网络的参数。

for step in range(STEPS):

    output, action, loss, grads = action_selection(model)
    if action == False:  # Action A
        reward = float(np.random.random() < 0.3)

    if action == True:  # Action B
        reward = float(np.random.random() < 0.4)

    grads_adjusted = []
    for var_index in range(len(model.trainable_variables)):
        grads_adjusted.append((reward-0.5)*2 * grads[var_index])

    model.trainable_variables[0].assign(model.trainable_variables[0]-LR*grads_adjusted[0])
    model.trainable_variables[1].assign(model.trainable_variables[1]-LR*grads_adjusted[1])

下图总结了学习过程的演变。

使用策略梯度强化学习进行 A/B 优化的学习过程演变。(图片由作者创建)

在前图显示的 1000 次广告展示的活动中,总共得到了 393 次点击,这与我们如果仅选择更好的广告 B 预期的 400 次点击非常接近。我们首先通过观察所有图表在初始步骤 = 1 来回顾学习过程。我们观察到神经网络输出从 0.5 开始,导致广告 B 和广告 A 的选择概率均为 50%。binary_crossentropy_loss 测量模型输出与实际采取的行动之间的差异。由于行动要么是 0,要么是 1,初始损失值为模型输出 0.5 的负对数,大约为 0.7。由于我们在神经网络中只有一个神经元,梯度包含该神经元的权重和偏差的两个标量值。如果选择了广告 A,偏差的梯度是正数;如果选择了广告 B,偏差的梯度是负数。权重参数的梯度始终为零,因为神经网络的输入为零。奖励是高度随机的,因为广告点击的概率只有 30%-40%。如果广告被点击,我们得到奖励,梯度保持不变,否则我们会反转梯度。调整后的梯度乘以学习率并从神经网络的初始参数中减去。我们可以看到,偏差值从零开始,当应用正的调整梯度时变得更负,而当应用负的调整梯度时变得更正。

在广告活动期间,神经网络的输出趋向于一,从而增加了选择广告 B 的机会。然而,即使模型输出已经接近一,广告 A 仍有小概率被展示。当模型输出接近一时,如果选择了动作 B,会有小的损失值和小的负梯度,但在选择广告 A 的少数情况下,会得到较大的损失值,表现为偶尔的峰值和较大的正梯度。在收集奖励后,观察到一些正峰值在调整梯度中被反转,因为这些动作没有带来点击。由于广告 B 的点击概率较高,因此小的负调整梯度比广告 A 点击产生的正梯度更频繁地应用。因此,模型的偏差值逐步增加,而在稀有的情况下,如果广告 A 被选择并点击,偏差值会减少。模型的输出由应用于模型偏差值的 sigmoid 函数提供。

学习率的影响

在本演示中,我们观察到神经网络可以学习选择两个选项中的较优者,并更频繁地应用该选项以最大化奖励。在这种设置下,平均将获得 391 次点击,其中广告 A 的点击概率为 30%,广告 B 的点击概率为 40%。在实际操作中,这些概率会低得多,而且它们之间的差异可能会更小,使得神经网络更难探索更好的选项。策略梯度方法的优点在于自动调整探索与利用之间的平衡。然而,这一平衡会受到学习率的影响。较高的学习率将导致较短的探索阶段和更快地应用所学策略,如下图所示,学习率从 0.01 增加到 10. 模型输出在 100 个单独广告活动中平均随着学习率的增加而更快地增长,直到学习率为 1. 然而,在较高的学习率下,存在适应错误行动的风险,这种行动可能仅在短暂的探索期内显得更好。在高学习率下,模型输出调整得过快,导致决策不稳定。

学习率对神经网络输出的影响。(图像由作者创建)

因此,存在一个最佳学习率需要选择,这在实践中可能很难找到,因为事先不知道点击概率。将学习率从 0.01 变化到 10.0 可以看到,在 0.1 到 2.0 之间的学习率可以获得点击总数的广泛最大值。较高的学习率明显增加了标准偏差,显示了学习过程的不稳定性,同时也导致了点击总数的减少。

学习率对广告活动期间获得的总点击数的影响。(图像由作者创建)

摘要

本演示展示了如何将强化学习用于 A/B 优化。它是一个简单的例子,用于说明策略梯度方法的基本过程。我们已经学习了神经网络如何根据调整后的梯度更新其参数,具体取决于所选择的广告是否被点击。应用所学策略可以迅速最大化点击率。然而,选择最佳学习率在实际操作中可能比较困难。

你可以在 huggingface.co 上找到完整的代码和一个 streamlit 演示:huggingface.co/spaces/Bernd-Ebenhoch/AB_optimization

像专家一样进行 A/B 测试:掌握统计测试选择的艺术

原文:towardsdatascience.com/a-b-testing-like-a-pro-master-the-art-of-statistical-test-selection-603eb5803586?source=collection_archive---------7-----------------------#2023-12-29

现实世界中的数据可能很棘手!使用 Python 选择和应用正确统计测试的指南

Thauri DattadeenTowards Data Science Thauri Dattadeen

·

Follow 发布于 Towards Data Science ·15 min read·2023 年 12 月 29 日

--

A/B 测试是强大的工具,但选择错误的统计测试可能导致误导性结果。本指南将帮助你为数据选择完美的测试,以确保可靠的分析并做出自信的推荐。

Jason Dent 拍摄的照片,刊登于 Unsplash

刚刚完成了你的 A/B 测试?激动人心的时刻还未结束!真正的魔力在于深入数据,发掘有价值的见解。本指南为你,数据分析师或数据科学家,提供了一种系统的方法来分析你的 A/B 测试结果。

分析的一个重要部分涉及理解数据以及选择正确测试的复杂统计基础。这一步骤通常被忽视,因为 跳过实施直接开始分析的诱惑,可能会错过关键的见解。

根据你正在分析的内容,需要做出不同的一组假设,因此选择的测试也不同。本文将指导你如何为你的数据选择‘正确’的测试。

指南

直接进入正题。这张表集中于典型移动应用程序的指标,这些指标是 A/B 测试的典型目标,尽管这些原则和假设适用于所有情况。

指标概述及推荐的图表(作者提供)

跳到下面必要的部分,我将描述每种指标类型,如何决定最佳测试,并使用 Python 进行计算!

第一部分:每用户平均指标

第二部分:分类指标

第三部分:联合指标

在开始之前,基本定义:

零假设:

每个假设检验由一个“理论”组成,这被称为 零假设 (H₀)。我们分析的目标是尝试自信地证明这个假设是否正确。

零假设 (H₀) 假定两个组之间没有差异,即该特征没有影响。

例如 H₀: μ1 = μ2

HA: μ1 ≠ μ2

显著性水平 (Alpha):

显著性水平,或 alpha (α),是用于决定测试结果是否具有统计学显著性的一个指标。这是运行测试之前做出的基本假设的一部分。

简而言之,显著性水平帮助你确定你的发现是否可靠,而不仅仅是偶然事件。它充当了一个阈值,用于说“嘿,这可能是真的,而不仅仅是巧合。”

P 值:

p 值或概率值,是一种用于统计学中帮助确定对零假设的证据强度的度量。

简而言之,p 值与获得假阳性结果的概率有关,即我们得到的数据由于偶然性而发生,我们犯了第一类错误的概率。

如果 p 值很高,我们不能相信数据,因为假阳性结果的可能性很高。如果数据不可靠(即 p 值很高),我们不能自信地证明或否定 H₀。

P 值 < 显著性水平 → 拒绝 H₀。 我们有足够的数据来得出两个组之间存在显著差异的结论。

P 值 >显著性水平 → 不拒绝 H₀。 我们没有足够的数据得出两个组有显著差异的结论

选择要分析的指标

在开始之前,理解你从 A/B 测试中想要分析的内容是很重要的。

例如,如果你在重新设计首次用户体验(FTUE),你可能会关注用户保留或转化等指标。这些通常涉及是/否(1 或 0)的结果,使得“两个比例 Z 检验”成为一个不错的选择。

我们将探索上面指南中提到的不同类型的指标,解释为什么选择它们以及如何确保你的数据符合测试要求。

设置数据

我假设你已经拥有数据,并且数据格式为包含 3 列的表格:唯一 ID、变体、指标。

#Separate the data into 2
group_A = df[df['Variant'] == "A"]["Metric"]
group_B = df[df['Variant'] == "B"]["Metric"]
#Change "A" and "B" to the relevant groups in your dataset
#Change "Metric" to the relevant column name with the values

第一部分:每用户平均指标

这是最常见的指标分析类型,涉及数据的独立样本。

在大多数实际情况下,每用户平均指标,如每用户平均收入或每用户平均花费时间,都是非常常见的分析对象。如果样本量足够大,你可以直接跳过这一步,选择 Welch 的 t 检验。

选择适合每个用户指标的测试流程图(作者提供)

我将逐一讲解每个流程,并描述决定使用哪条路径的步骤 -

流程 1:大样本或正态分布:

上述流程图中的流程 1(作者提供)

我对“大样本”的一般假设通常是个体样本数量大于 10,000,尽管这个定义是相对的,可能会因具体研究领域、进行的分析类型和需要检测的效应大小而有所不同。

如果 n 相对较小,则执行正态性检验以确定是否选择 T 检验。有几种方法可以检验正态性。最简单的方法是通过创建直方图并目视检查数据。如果数据大致呈现正态分布,则继续。如果仍不确定,最好进行更具统计性的检验,例如 Shapiro-Wilkes 正态性检验。这里有一篇关于各种正态性检验方法的好文章。请记住,每种统计检验通常对数据有不同的假设,因此在选择正态性检验之前请牢记这一点。

请参见下面的代码片段,展示如何通过创建直方图进行视觉检查或使用名为 Shapiro-Wilkes 检验的统计检验来测试数据的正态性。

正态性检验

import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as stats

alpha=0.05 #We assume an alpha here of 0.05

# Creating Histograms
grouped = df.groupby('Variant') 
#Variant column identifies the group of the user - group_A / group_B etc

# Plotting histograms for each group to visually inspect the shape of the data
for name, group in grouped:
    plt.hist(group['Metric'])
    plt.title(name)
    plt.show()

# Shapiro-Wilkes Test to statistically test for normality
for name, group in grouped:
    shapiro_test = stats.shapiro(group['Value'])
    print(f"Shapiro-Wilk Test for Group {name}: W = {shapiro_test[0]}, p-value = {shapiro_test[1]}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

流程 2a:方差是否相等?

上述流程图中的流程 2a(作者提供)

接着上面“每用户平均”流程图 —

如果你已经确认数据符合正态分布,那么下一步是检查数据集是否具有相等的方差。这将决定你应该使用 Welch 的 t 检验还是 Student 的 t 检验。

Welch 的 t 检验和 Student 的 t 检验之间的主要区别在于自由度和样本方差估计。Student 的检验假设两个样本具有相同的方差,而 Welch 的检验则不要求。

在比较大样本量(n > 10,000)时使用 Welch 的 t 检验或 Student 的 t 检验进行假设检验时,两者之间的显著性水平差异通常可以忽略不计。这是因为在处理大样本量时,Student 的 t 检验对方差相等假设的依赖对检验准确性的影响非常小。

即使方差相等的假设被违反,Student 的 t 检验仍然相对稳健,这意味着它能产生准确的 p 值,并保持所需的 I 型错误率(拒绝真实零假设的概率)。这种稳健性源于中心极限定理,该定理指出,随着样本量的增加,样本均值的分布趋近于正态分布,而不管基础总体分布如何。

相比之下,Welch 的 t 检验专门设计用于处理不等方差的情况,使其在方差相等的假设存在疑问时更为合适。然而,对于大样本量而言,Welch 的 t 检验与 Student 的 t 检验之间的显著性水平差异通常很小。

如果你担心存在不等方差的可能性,Welch 的 t 检验是一个更安全的选择。然而,如果你想最大化效能,并且对样本量足够大的信心很足,那么可以使用 Student 的 t 检验。

参见下面的代码片段,展示如何使用 Bartlett 检验来测试数据的方差是否相等。Bartlett 检验是一种非常稳健的检验,但它要求数据符合正态分布。如果你更倾向于使用一个不那么稳健的检验,那么Levene 检验可能更为合适。

方差相等性检验

from scipy.stats import bartlett, levene

# Perform Bartlett's test for equal variances (works best on data that conforms to normality)
statistic, p_value = bartlett(group_A, group_B)

# Perform Levene's test for equal variances (less sensitive to Normality assumption)
statistic, p_value = levene(group_A, group_B)

# Display test results
print(f"Test statistic: {statistic}")
print(f"P-value: {p_value}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference in variances between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference in variances between the groups.")

流程 2b:中位数还是均值?

上述流程图中的流程 2b(作者提供)

继续以上的用户平均值流程图 —

如果你对数据集是否符合正态分布有合理的疑虑,那么可能需要使用另一种统计方法来分析数据集。下一步是决定数据的均值还是中位数更为有用。

在 A/B 测试中考虑中位数而不是均值在特定情况下可能有益,尤其是当数据可能受到异常值或极度偏斜的非正态分布影响时。

  • 结果沟通: 使用中位数可以提供更清晰和直观的中心趋势解释,特别是在描述典型或每用户的行为时。这可能对利益相关者或非技术观众更具关联性。

  • 偏态分布: 如果你的数据高度偏斜或不遵循正态分布,均值可能无法准确代表典型值。在这种情况下,中位数提供了一个更稳健的中心趋势估计,因为它不容易受到极端值或分布形状的影响。

  • 离群值敏感性: 均值对数据集中的离群值或极端值高度敏感。即使少量的离群值也能显著影响均值,使其无法代表中心趋势。相比之下,中位数不容易受到极端值的影响,因为它代表了按升序排列的数据集中的中间值。

这两种测量方法各有优点,选择其中一种应该与数据的性质、离群值的影响以及你希望从 A/B 测试中得出的具体见解相一致。在得出结论时,你应该考虑均值和中位数。

这是关于曼-惠特尼 U 检验的非常有用的指南。一如既往,在开始之前最好先做一些研究,彻底理解每个检验方法!

统计检验

如果你遵循了上面的每用户平均值流程图,你现在应该已经决定了什么是确定两组指标是否具有统计显著性差异的最佳检验方法。请参见下面如何进行这些检验。

请参考上述指南和每用户平均值流程图。

学生 t 检验

import scipy.stats

# Student's t-test - This test requires Normality and Equal Variances
t_statistic, p_value = stats.ttest_ind(group_A, group_B)
print(f"Student's t-test: t = {t_statistic}, p-value = {p_value}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

Welch 检验

import scipy.stats

# Welch's t-test - This test requires Normality
t_statistic, p_value = stats.ttest_ind(group_A, group_B, equal_var=False)
print(f"Welch's t-test: t = {t_statistic}, p-value = {p_value}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

曼-惠特尼 U 检验

# Mann-Whitney U-test - No statistical assumptions, Median preferred over Mean
u_statistic, p_value = stats.mannwhitneyu(group_A, group_B)
print(f"Mann-Whitney U-test: U = {u_statistic}, p-value = {p_value}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

自助法

#Bootstrapping - forNon-Normal data/Small sample sizes, and Mean is preferred

# Calculate observed difference in means
observed_diff = np.mean(group_B) - np.mean(group_A)

# Combined data
combined_data = np.concatenate((group_A, group_B))

# Number of bootstrap samples
num_samples = 10000  # You can adjust this number based on computational resources

# Bootstrap resampling
bootstrap_diffs = []
for _ in range(num_samples):
    # Resample with replacement
    bootstrap_sample = np.random.choice(combined_data, size=len(combined_data), replace=True)

    # Calculate difference in means for each bootstrap sample
    bootstrap_mean_A = np.mean(bootstrap_sample[len(group_A):])
    bootstrap_mean_B = np.mean(bootstrap_sample[:len(group_A)])
    bootstrap_diff = bootstrap_mean_B - bootstrap_mean_A

    bootstrap_diffs.append(bootstrap_diff)

# Calculate p-value (significance level)
p_value = np.mean(np.abs(bootstrap_diffs) >= np.abs(observed_diff))

print(f"P-value: {p_value}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

第二部分:分类变量

选择适合的分类数据检验的流程图(作者提供)

在这一部分,我们将探讨分类指标。这些指标可以是离散的,例如点击/未点击,或者在多变量检验的情况下是连续的。

遵循上面的流程图选择最适合你数据的检验方法。

2 组

两比例 Z 检验(二元指标)

这些指标包括留存、转化、点击等。

二项变量的两样本 z 检验用于比较两个组之间的二元结果比例。从统计角度来看,随着 N 的增大,二项分布趋向于正态分布。这一假设通常适用,因此使用 z 检验是合理的。

H0: μ1 — μ2 = 0

HA: μ1 — μ2 ≠ 0

两比例 Z 检验

from statsmodels.stats.weightstats import ztest

# Calculate the z-statistic and p-value. This assumes binomially distributed and i.i.d. variables.
z_stat, p_value = ztest(group_A, group_B)

print(f"Two Sample z-test: t = {z_stat}, p-value = {p_value}")

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

3 组及以上

离散变量

皮尔逊卡方检验

卡方检验是另一个强大的工具,特别是在 A/B 测试中有多个组(除了对照组)时,尤其有效。

它允许你同时比较多个变体,而无需假设任何分布特性。该方法也适用于类似的二元变量,但使用多个组而不是仅有的 2 个。由于这些组可能会将样本量分割成更小的组,因此必须确保每个组的样本量保持相对较大。

皮尔逊卡方检验用于确定在多个组中观察到的分类数据频率与期望频率之间是否存在显著关联或差异。因此,零假设假定组间没有差异。

由于数据是离散的,我们创建一个列联表来汇总每个变体的计数。然后由 stats.chi2_contingency 函数进行解释。

皮尔逊卡方检验

# Create a contingency table
contingency_table = pd.crosstab(df['Variant'], df["metric"])

# Perform the chi-squared test
chi2, p_value, dof, expected = stats.chi2_contingency(contingency_table)

if p_value < alpha:
    print("There is a statistically significant difference in the distribution of the metric across groups.")
else:
    print("There is no statistically significant difference in the distribution of the metric across groups.")

连续变量

ANOVA 检验

ANOVA 是一种用于比较三个或更多组均值的统计检验。它评估组均值之间观察到的差异是否可能是偶然的,还是实际上代表了它们底层总体中的显著差异。它在发布多个不同变体时很有用,可以将结果相互比较,以节省时间,而不是部署单独的 A/B 测试。

ANOVA 对正态性和方差齐性假设的违反相对鲁棒,特别是当样本量相对较大时。

ANOVA 检验

# Group the counts for the various groups in the data
grouped_data = [df[df['Variant'] == cat]['Metric'] for cat in df['Variant'].unique()]

# Perform ANOVA test
f_statistic, p_value = stats.f_oneway(*grouped_data)

if p_value < alpha:
    print("There is a statistically significant difference in the means of the metric across groups.")
else:
    print("There is no statistically significant difference in the means of the metric across groups.")

该检验确定组是否具有统计显著性。我们在这里遇到的一个问题是尝试识别出一个显著优于其他组的特定组。虽然总体 ANOVA 检验突出了普遍的偏差,但需要更深入的调查来识别展示统计差异的特定组。

幸运的是,有一个简洁的函数使用了 Tukey 的范围检验,提供了一种结构化的方法。它生成了一个全面的成对比较表,揭示了各组之间的统计显著差异。

然而,由于 Tukey 的范围检验可能违反了基本假设,必须谨慎使用。该检验应主要用于识别不同组,仅作为组间比较的辅助工具。这个视频非常有用,展示了如何执行和使用

参见下面的代码片段,应作为上述 ANOVA 检验的辅助工具,用于识别优于其他组的特定组。

Tukey 的 HSD 检验

import statsmodels.api as sm
from statsmodels.stats.multicomp import pairwise_tukeyhsd

# Perform Tukey's HSD test for post-hoc analysis
tukey = pairwise_tukeyhsd(df["metric"], df['Variant'])

# Print pairwise comparison results
print(tukey.summary())

第三部分:联合指标

根据上述指南,如果你确定你的指标实际上是两个或更多变量的联合指标,那么你可能需要采取额外步骤来有效确定各组之间的统计差异。这是因为上述其他检验假设你测试的指标彼此独立。

Delta t 检验

Delta t 检验是一种统计方法,用于评估两个独立组之间均值的差异,同时考虑形成联合分布的两个随机变量的比率。在 A/B 测试领域,常常会遇到度量本身可能不独立的情况。

联合分布示例(作者提供)

一个例子是广告点击率(Ad Click-Through-Rate)。一个人可能会多次查看同一个广告,但只点击一次。

使用标准 t 检验的问题在于,我们分析的是两个独立的随机变量,它们的比率形成了一个联合分布。虽然对象本身是独立的,但它们的联合比率却不是。这违反了学生 t 检验和 Welch t 检验的独立性假设。

相反,我们使用 Delta 方法来估计方差;

方差估计公式(见下文参考)

参见下面的代码片段,展示了如何计算这个新的方差公式,并使用这个新方差创建 t 检验函数。

Delta t 检验

# create the new Variance function as described above
def var_ratio(metric1,metric2): 
     mean_x = np.mean(metric1)
     mean_y = np.mean(metric2)
     var_x = np.var(metric1,ddof=1)
     var_y = np.var(metric2,ddof=1)
     cov_xy = np.cov(metric1,metric2,ddof=1)[0][1]
     result = (mean_x**2 / mean_y**2) * (var_x/mean_x**2 - 2*cov_xy/(mean_x*mean_y) + var_y/mean_y**2)
     return result

 # create this new ttest function, using the new Variances above. This is a standard t-test function.
def delta_ttest(mean_c,mean_t,var_c,var_t, alpha = 0.05):
     mean_diff = mean_t - mean_c
     var = var_c + var_t
     std_e = stats.norm.ppf(1 - alpha/2) * np.sqrt(var)
     lower_ci = mean_diff - std_e 
     upper_ci = mean_diff + std_e
     z = mean_diff/np.sqrt(var)
     p_val = stats.norm.sf(abs(z))*2
     return z, p_val, upper_ci, lower_ci

#Eg. Here we calculate the significance of the CTR for a control and treatment group. 
var_c = var_ratio(control['click'],control['view']) #Calculates the delta variance for the control group
var_t = var_ratio(treatment['click'],treatment['view']) #Calculates the delta variance for the treatment group
mean_c = control['click'].sum()/control['view'].sum()
mean_t= treatment['click'].sum()/treatment['view'].sum()
z, p_value, upper_ci, lower_ci = delta_ttest(mean_c,mean_t,var_c,var_t,alpha) #Applies the ttestusing these new delta variances

if p_value < alpha:
    print("Reject the null hypothesis. There is a significant difference between the groups.")
else:
    print("Fail to reject the null hypothesis. There is no significant difference between the groups.")

结论:

总结来说,虽然 A/B 测试对于实验和优化至关重要,但选择正确的统计检验方法至关重要。为了获得稳健和可靠的结果,数据科学家应仔细考虑特征和检验假设。

记住,A/B 测试是强大的工具,但选择错误的统计检验方法可能会导致误导性的结果!

参考文献

1. Sivasai Yadav Mudugandla 的正态性检验

2. Kyaw Saw Htoon 的 Levene 检验

3. Ricardo Lara Jácome 的 Mann Whitney U 检验

4. Delta 方法:arxiv.org/pdf/1803.06336.pdf

5. Ahmad Nur Aziz 的 Delta T 检验

使用 R 和 brms 对学校毕业生结果进行贝叶斯比较

原文:towardsdatascience.com/a-bayesian-comparison-of-school-leaver-outcomes-with-r-and-brms-4da9ae5f9895

ANOVA — 贝叶斯风格

Murray GillinTowards Data Science Murray Gillin

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

--

我们在离开学校时常常会思考我们想做什么。作为小孩子时,我们被问及长大后想做什么,然后在 13 年的前高等教育中度过。在公共政策中,政府、天主教和独立学校系统之间的差异被广泛讨论,特别是在非政府学校的公共资助方面,以及如何分配资源。

给定学校部门,学生结果之间是否存在实际差异?

在澳大利亚维多利亚州,州政府每年进行一次调查,以评估高中毕业后的结果 (On Track Survey)。该 数据集 (在 CC BY 4.0 下提供)覆盖了多个年份,我们将只查看本文编写时最新的 2021 年数据。

本文将应用贝叶斯分析方法,使用 R 包 brms 来回答这些问题。

图片由 Good Free Photos 提供,来源于 Unsplash

加载库和数据集

以下是我们加载所需包的数据集,该数据集以宽格式报告呈现,包含许多合并单元格。R 不喜欢这种格式,因此我们需要做一些硬编码来重新标记向量,并创建一个整洁的数据框。

library(tidyverse) #Tidyverse meta package
library(brms) #Bayesian Modelling Package
library(tidybayes) #Tidy Helper Functions and Visualisation Geoms for Bayesian Models
library(readxl)
df <- read_excel("DestinationData2022.xlsx", 
    sheet = "SCHOOL PUBLICATION TABLE 2022", 
    skip = 3)
colnames(df) <- c('vcaa_code', 
                  'school_name', 
                  'sector', 
                  'locality', 
                  'total_completed_year12', 
                  'on_track_consenters', 
                  'respondants', 
                  'bachelors', 
                  'deferred', 
                  'tafe', 
                  'apprentice_trainee', 
                  'employed', 
                  'looking_for_work', 
                  'other') 

df <- drop_na(df)

下面是我们数据集的一个示例。

初始数据框的示例(作者提供的图片)

数据集有 14 个字段。

  • VCAA 代码 — 每个代码的行政 ID

  • 学校名称

  • 部门 — G = 政府,C = 天主教,I = 独立

  • 地域或郊区

  • 完成年级 12 的总学生数

  • 跟踪同意者

  • 受访者

  • 其他列表示每个结果的受访者百分比

对于我们的横截面研究,我们对按部门划分的结果比例感兴趣,因此我们需要将这个宽格式数据框转换为长格式。

df_long <- df |> 
  mutate(across(5:14, ~ as.numeric(.x)), #convert all numeric fields captured as characters
         across(8:14, ~ .x/100 * respondants), #calculate counts from proportions
         across(8:14, ~ round(.x, 0)), #round to whole integers
         respondants = bachelors + deferred + tafe + apprentice_trainee + employed + looking_for_work + other) |> #recalculate total respondents |> 
  filter(sector != 'A') |>  #Low volume 
  select(sector, school_name, 7:14) |> 
  pivot_longer(c(-1, -2, -3), names_to = 'outcome', values_to = 'proportion') |> 
  mutate(proportion = proportion/respondants)

具有兴趣特征的长格式数据框(作者提供的图片)

探索性数据分析

让我们简要地可视化并总结一下我们的数据集。政府部门有 157 所,独立学校有 57 所,天主教学校有 50 所。

df |> 
  mutate(sector = fct_infreq(sector)) |> 
  ggplot(aes(sector)) +
    geom_bar(aes(fill = sector)) + 
    geom_label(aes(label = after_stat(count)), stat = 'count', vjust = 1.5) +
    labs(x = 'Sector', y = 'Count', title = 'Count of Schools by Sector', fill = 'Sector') +
    scale_fill_viridis_d(begin = 0.2, end = 0.8) +
    theme_ggdist()

按部门分类的学校数量(作者提供的图片)

df_long |> 
  ggplot(aes(sector, proportion, fill = outcome)) +
    geom_boxplot(alpha = 0.8) +
    geom_point(position = position_dodge(width=0.75), alpha = 0.1, color = 'grey',  aes(group = outcome)) +
    labs(x = 'Sector', y = 'Proportion', fill = 'Outcome', title = 'Boxplot of Respondant Proportions by Sector and Outcome') +
    scale_fill_viridis_d(begin = 0.2, end = 0.8) +
    theme_ggdist()

按部门和结果分布的比例(作者提供的图片)

分布情况讲述了一个有趣的故事。学士结果在所有部门中变异性最大,独立学校的学生选择攻读本科教育的中位比例最高。值得注意的是,政府学校在高中毕业后就业的学生比例中位数最高。所有其他结果的变化不大 — 我们会很快重新审视这一点。

统计建模 — Beta 似然回归

我们关注的是按学校划分的学生比例及其高中毕业后的结果。在这些情况下,beta 似然是我们最好的选择。brms 是由Buerkner开发的一个出色的包,用于构建贝叶斯模型。我们的目标是对按结果和部门划分的比例进行建模。

Beta 回归模型有两个参数,μ 和 φ。μ 代表平均比例,φ 是精度(或方差)的度量。

我们的第一个模型如下所示,请注意我们已经开始考虑部门和结果之间的交互,因为这是我们希望模型回答的问题,因此这是一个 ANOVA 模型。

我们要求模型为每个部门和结果的组合创建单独的 Beta 项,并使用一个汇总的 φ 项,或使用相同方差的不同比例均值。我们期望这些比例中的 50% 位于 logit 规模上的(-3, 1)之间或比例上的(0.01, 0.73)之间。这是足够宽泛但有信息的先验。全球 Phi 估计值是一个正数,因此我们使用一个足够宽泛的 gamma 分布。

模型 m3a 的数学形式 — 作者提供的图片

# Modelling Proportion with Sector:Outcome Interaction term using Beta - Pooled Phi term

m3a <- brm(
  proportion ~ sector:outcome + 0, 
  family = Beta,
  data = df_long |> mutate(proportion = proportion + 0.01), # Beta regression can't take outcomes that are 0 so we fudge here by adding tiny increment
  prior = c(prior(normal(-1, 2), class = 'b'),
            prior(gamma(6, 1), class = 'phi')),
  iter = 8000, warmup = 1000, cores = 4, chains = 4, seed = 246, save_pars = save_pars(all = T),
  control = list(adapt_delta = 0.99, max_treedepth = 15)
) |> 
  add_criterion(c('waic', 'loo'), moment_match = T)

summary(m3a)

模型 m3a 的输出摘要 — 作者提供的图片

注意在模型设置中,我们对所有比例增加了 1% — 这是因为 Beta 回归无法处理零值。我们尝试使用零膨胀 Beta 来建模,但收敛所需时间更长。

同样地,我们可以在没有合并 phi 的情况下建模,这在考虑到我们在上述分布中观察到的情况时直观上是有意义的,因为每个部门和结果组合中存在变异。模型定义如下。

m3b <- brm(
  bf(proportion ~ sector:outcome + 0,
     phi ~ sector:outcome + 0),
  family = Beta,
  data = df_long |> mutate(proportion = proportion + 0.01),
  prior = c(prior(normal(-1, 2), class = 'b')),
  iter = 8000, warmup = 1000, cores = 4, chains = 4, seed = 246, save_pars = save_pars(all = T),
  control = list(adapt_delta = 0.99)
) |> 
  add_criterion(c('waic', 'loo'), moment_match = T)

summary(m3b)

m3b 的输出总结 — 作者提供的图片

模型诊断与比较

手中有两个模型后,我们将比较它们在样本外的预测准确性,使用贝叶斯 LOO 估计的期望对数逐点预测密度 (elpd_loo)。

comp <- loo_compare(m3a, m3b)

print(comp, simplify = F)

模型 m3a 和 m3b 的 LOO 比较 — 作者提供的图片

简而言之,预期的对数逐点留一值越高,对未见数据的预测准确性越大。这为我们提供了模型之间的相对准确性度量。我们可以进一步通过完成后验预测检查来验证这一点,即观察值和模拟值的比较。在我们的案例中,模型 m3b 更好地模拟了观察数据。

alt_df <- df_long |> 
  select(sector, outcome, proportion) |> 
  rename(value = proportion) |> 
  mutate(y = 'y', 
         draw = 99) |> 
  select(sector, outcome, draw, value, y)

sim_df <- expand_grid(sector = c('C', 'I', 'G'),
            outcome = unique(df_long$outcome)) |> 
  add_predicted_draws(m3b, ndraws = 1200) |> 
  rename(value = .prediction) |> 
  ungroup() |> 
  mutate(y = 'y_rep',
         draw = rep(seq(from = 1, to = 50, length.out = 50), times = 504)) |> 
  select(sector, outcome, draw, value, y) |> 
  bind_rows(alt_df)

sim_df |> 
  ggplot(aes(value, group = draw)) +
    geom_density(aes(color = y)) +
    facet_grid(outcome ~ sector, scales = 'free_y') +
    scale_color_manual(name = '', 
                       values = c('y' = "darkblue",
                                  'y_rep' = "grey")) +
    theme_ggdist() +
    labs(y = 'Density', x = 'y', title = 'Distribution of Observed and Replicated Proportions by Sector and Outcome')

模型 m3a 的后验预测检查 — 作者提供的图片

模型 m3b 的后验预测检查 — 作者提供的图片

模型 m3b 的非合并方差或 phi 项能够更好地捕捉按部门和结果分布的变异。

ANOVA — 贝叶斯风格

记住,我们的研究问题是关于了解部门间的结果差异及其程度。在频率统计学中,我们可能使用 ANOVA,即组间均值差异的方法。这种方法的弱点在于结果提供了一个估计值和置信区间,但没有关于这些估计的不确定性,并且提供了一个反直觉的 p 值来说明均值差异是否具有统计显著性。不,谢谢。

以下,我们生成每个部门和结果组合交互的期望值集合,然后使用优秀的 tidybayes::compare_levels() 函数进行繁重的计算。它计算了每个结果的部门间后验均值差异。为了简洁起见,我们排除了“其他”结果。

new_df <- expand_grid(sector = c('I', 'G', 'C'), 
          outcome = c('apprentice_trainee', 'bachelors', 'deferred', 'employed', 'looking_for_work', 'tafe'))

epred_draws(m3b, newdata = new_df) |> 
  compare_levels(.epred, by = sector, comparison = rlang::exprs(C - G, I - G, C - I)) |> 
    mutate(sector = fct_inorder(sector),
         sector = fct_shift(sector, -1), 
         sector = fct_rev(sector))  |> 
  ggplot(aes(x = .epred, y = sector, fill = sector)) +
      stat_halfeye() +
      geom_vline(xintercept = 0, lty = 2) + 
      facet_wrap(~ outcome, scales = 'free_x') +
      theme_ggdist() +
      theme(legend.position = 'bottom') +
      scale_fill_viridis_d(begin = 0.4, end = 0.8) +
      labs(x = 'Proportional Difference', y = 'Sector', title = 'Differences in Posterior Means by Sector and Outcome', fill = 'Sector')

贝叶斯 ANOVA — 作者提供的图片

或者,我们可以用一个整洁的表格总结所有这些分布,以便更容易解释和一个 89% 的可信区间。

marg_gt <- epred_draws(m3b, newdata = new_df) |> 
  compare_levels(.epred, by = sector, comparison = rlang::exprs(C - G, I - G, C - I)) |> 
  median_qi(.width = .89) |> 
  mutate(across(where(is.numeric), ~round(.x, 3))) |> 
  select(-c(.point, .interval, .width)) |> 
  arrange(outcome, sector) |> 
  rename(diff_in_means = .epred, 
         Q5.5 = .lower, 
         Q94.5 = .upper) |> 
  group_by(outcome) |> 
  gt() |> 
  tab_header(title = 'Sector Marginal Distributions by Outcome') |> 
  #tab_stubhead(label = 'Sector Comparison') |> 
  fmt_percent() |> 
  gtExtras::gt_theme_pff()

按部门和结果的后验均值差异总结表及 89% 可信区间 — 作者提供的图片

例如,如果我们要在正式报告中总结比较,我们可能会写以下内容。

政府学校的学生在完成 VCE 后开始本科学位的可能性低于天主教和独立学校的同学。

平均而言,42.5%(介于 39.5%和 45.6%之间)的政府学校学生,53.2%(介于 47.7%和 58.4%之间)的天主教学校学生和 65%(介于 60.1%和 69.7%之间)的独立学校学生在完成 12 年级后开始接受本科教育。

在 89%的后验概率下,天主教学校与政府学校学生本科入学的差异在 5.6%到 15.7%之间,平均值为 10.7%。此外,独立学校与政府学校学生本科入学的差异在 17.8%到 27%之间,平均值为 22.5%。

这些差异是显著的,差异不为零的概率为 100%。

总结与结论

在本文中,我们展示了如何使用贝塔似然函数和贝叶斯建模来建模比例数据,然后执行贝叶斯方差分析(ANOVA)以探索不同部门之间比例结果的差异。

我们没有试图创建这些差异的因果理解。可以想象,有几个因素影响个体学生的表现,包括社会经济背景、父母的教育水平,以及学校层面的影响、资源等。这是一个极其复杂的公共政策领域,常常被零和博弈的 rhetoric 所困扰。

就个人而言,我是我大家庭中第一个接受并完成高等教育的人。我上了一所中等水平的公立高中,并取得了相当不错的成绩,顺利进入了我首选的学校。我的父母鼓励我继续接受教育,他们在 16 岁时选择了辍学。虽然本文提供了政府学校和非政府学校之间差异的证据,但它本质上只是描述性的。

贝叶斯选择餐厅的方法

原文:towardsdatascience.com/a-bayesian-way-of-choosing-a-restaurant-87905a745854?source=collection_archive---------5-----------------------#2023-10-12

Kirill TsyganovTowards Data Science Kirill Tsyganov

·

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

--

最近我在寻找一家新的好餐厅。Google Maps 给了我两个选项:餐厅 A 有 10 条评论全是 5 星,而餐厅 B 有 200 条评论,平均评分为 4 星。我倾向于选择餐厅 A,但评论数量少让我感到担忧。另一方面,餐厅 B 的许多评论让我对其 4 星评级有信心,但没有承诺任何卓越。因此,我想比较这些餐厅,并选择最好的,考虑评论或缺少评论的情况。感谢贝叶斯,我们有办法做到这一点。

图片由作者制作。

贝叶斯框架允许我们对评分的初始分布做出假设,然后根据观察到的数据更新最初的信念。

设定初始信念 / 先验

  • 起初,我们对每个评分(从 1 到 5 星)的概率一无所知。因此,在任何评论之前,所有评分的可能性是相等的。这意味着我们从均匀分布开始,这可以表示为Dirichlet 分布(Beta 分布的推广)。

  • 我们的平均评分将是 (1+2+3+4+5)/5 = 3,这是概率集中最多的地方。

# prior prob. estimates sampling from uniform
sample_size = 10000
p_a = np.random.dirichlet(np.ones(5), size=sample_size)
p_b = np.random.dirichlet(np.ones(5), size=sample_size)

# prior ratings' means based on sampled probs
ratings_support = np.array([1, 2, 3, 4, 5])
prior_reviews_mean_a = np.dot(p_a, ratings_support)
prior_reviews_mean_b = np.dot(p_b, ratings_support)

图片由作者制作。

更新信念

  • 要更新初始信念,我们需要将先验信念乘以观察数据的似然性,再乘以先验信念。

  • 观察到的数据自然地用Multinomial 分布(Binomial 的推广)来描述。

  • 事实证明,Dirichlet 是 Multinomial 似然的共轭先验。换句话说,我们的后验分布也是 Dirichlet 分布,参数包含了观测数据。

图片由作者制作。

# observed data
reviews_a = np.array([0, 0, 0, 0, 10])
reviews_b= np.array([21, 5, 10, 79, 85])

# posterior estimates of ratings probabilities based on observed
sample_size = 10000
p_a = np.random.dirichlet(reviews_a+1, size=sample_size)
p_b = np.random.dirichlet(reviews_b+1, size=sample_size)

# calculate posterior ratings' means
posterior_reviews_mean_a = np.dot(p_a, ratings_support)
posterior_reviews_mean_b = np.dot(p_b, ratings_support)
  • 现在,A 的后验平均评分介于先验 3 和观察到的 5 之间。但 B 的平均评分变化不大,因为大量的评论超出了初始信念的影响。

图片由作者制作。

那么,哪个更好呢?

  • 回到我们最初的问题,“更好”意味着A 的平均评分大于 B 的平均评分的概率,即 P(E(A|data)>E(B|data))。

  • 在我的情况下,我得到了 85%的概率,即餐厅 A 比餐厅 B 更好。

# P(E(A)-E(B)>0)
posterior_rating_diff = posterior_reviews_mean_a-posterior_reviews_mean_b
p_posterior_better = sum(posterior_rating_diff>0)/len(posterior_rating_diff)

图片由作者制作。

贝叶斯更新允许我们结合先验信念,这在评论数量较少的情况下特别有价值。然而,当评论数量很大时,初始信念对后验信念的影响不大。

代码可在我的 GitHub上找到,我打算去餐厅 A。

面向初学者的应用科学介绍

原文:towardsdatascience.com/a-beginner-friendly-introduction-to-applied-science-dd60741a9b17

学习特征和统计分析的基础知识

Emma BoudreauTowards Data Science Emma Boudreau

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

--

照片由 Edge2Edge Media 提供,来源于 Unsplash

介绍

经验主义可能是形成对任何事物的现实世界理解最重要的方面。虽然轶事观察可能对更基础的事物有很大帮助,但我们实际希望被证明并接受为事实的事物必须通过经验主义来处理。这种经验主义的支柱当然是数据。数据非常重要,因为它为我们提供了对周围世界的洞察。然而,数据的一个大缺陷是,数据必须被解释,而这种解释确实需要一定程度的专业知识——这意味着可能有很多人无法根据数据得出一些经验性结论。将这些人和能够做出经验性结论的人区分开的当然是统计学专业知识。

统计学让我们能够分解数据中的不同内容,并对这些数据的不同方面做出观察。统计学应用于数据可以非常强大,常常推动全球范围内每天做出的最重要的决策。考虑到这一点,很容易理解为什么这样的技能可能是令人向往的、重要的,甚至可能是令人难以承受的。幸运的是,数学就是数学——数字就是数字——事情可能会变得混乱,但总有解释。当涉及到统计学时,一些相对基础的知识实际上可以走很远。所有这些对于许多现代学科都非常重要;从数据科学到分析和营销,简单的统计学通常是一个突出的练习,因此了解这些领域的统计学也可以非常有价值。

notebook

特征

在我们真正理解统计学、数据科学、机器学习或类似的领域之前,我们首先需要理解数据。数据由特征和它们的后续观察组成,这些构成了一个总体。总体通常指的是“我们所有的干净数据”,与样本相对,后者可能暗示我们在假设检验或类似背景下使用一些数据。这可能听起来有些混乱,但很简单记住:你的总体是你所有的数据,而样本是你的一部分数据。特征只是这些数据的一列,观察则是一行。例如,一个人的特征是头发颜色,而观察是金发。我们的特征是数据结构和测量内容的轮廓,而观察是对应于该轮廓的实际数据。

特征主要有三种类型:

  • 标签

  • 类别

  • 连续

标签

标签是所有特征中使用最少的一种,但经常可以用来区分不同的观察值。这些标签可以在机器学习或统计学背景下使用,但很少被使用。一个在统计学背景下作为特征使用的例子是,如果我们想测试某个年份是否具有统计学意义。例如,如果我们想测试 2021 年的人是否比 2001 年的人高,我们将从两个年份中各取样本,并测试统计学意义。在机器学习背景下,确实有一些实例中日期或名字可能被用来预测某些特征,我也见过这种情况。我曾经有过的一个实际例子是训练机器学习算法来预测不同物品在不同日期的市场价值。

类别

类别数据与标签数据很相似。虽然标签数据并没有提供太多关于观察值的信息,但类别数据往往确实代表了实际的测量。在某些方面,类别数据只是连续空间的一部分标签——这一点在深入了解类别数据时会更加明了……但为了理解我的意思,我需要引用一个例子。

我们可以认为,体重超过 300 磅的人被认为非常超重,体重在 200–300 磅之间的人相当超重,100–200 磅通常为平均水平,而低于 100 磅在特定人群中则为偏瘦。这些都是放在连续空间上的类别标签,但在许多方面可以提供更多关于数据的信息,同时简化了这一特征——使其更易于解释和简化。如果我们的统计只需要知道一个人是否超重,那么我们可能不需要知道确切的体重。这有助于缓解一定程度的偏差,防止模型拟合问题,并且有助于隐藏个人信息。

连续

连续特征是最具代表性的特征类型。这些特征可以是评级、仪器测量、温度等任何数值型数据。每一个连续特征似乎都形成了其自身的数值空间。随之而来的是均值、标准差,以及我们传统上用于统计分析的所有其他数据。对分类数据也可以做同样的处理,但在某些情况下,这些特征必须转化为统计学和计算机能够理解的语言——即数字。更常见的是,我们使用分类特征来区分样本中的不同观察。

在许多情况下,你将很可能需要对特征进行一些处理。这通常取决于你所处理的特征类型,这可能会非常棘手。如果你想了解如何处理这些不同类型的特征,这里有一些我写的相关文章:

## 特征基础:特征集分析入门

一个关于如何更好地处理和理解特征的小指南

towardsdatascience.com [## 分类数据与连续数据:你需要了解的一切

对两种最常见的特征类型的概述,以及一些重要的注意事项。

chifi.dev](https://chifi.dev/categorical-data-vs-continuous-data-everything-you-need-to-know-36c2a0dbf6c?source=post_page-----dd60741a9b17--------------------------------) ## 编码器——如何编写,如何使用

通过这篇“从零开始”的指南,发现编码器在机器学习中的多种用途!

towardsdatascience.com

分析

现在我们对数据和可能遇到的不同特征有了基本了解,我们可以开始分析数据。为了使这一过程尽可能易于理解,我将使用一些数据来演示这些不同的概念。我将使用的数据是由佛罗里达州立大学分发的 trees.csv 数据,您可以在 这里 找到(GNU LGPL 许可证)。我将使用 Julia 编程语言,但这个过程在大多数不同的解决方案中通常是相同的,方法调用或技术可能会有所不同,但我们所做的技术和最终更改是相同的。我们将通过读取数据并进行一些基本处理来开始这个过程。这将需要一些 Julia 包:

using Pkg; Pkg.activate("ds")
Pkg.add("DataFrames")
Pkg.add("CSV")
Pkg.add("Statistics")
Pkg.add("HypothesisTests")

现在让我们导入数据,我将使用 sink 参数来读取我们的 DataFrame。sink 参数允许我们的数据格式模块推断如何将一些数据读取到给定的类型;在我们的案例中,就是 DataFrames。

using DataFrames
using Statistics
using HypothesisTests

在大多数情况下,这将是我们清理数据和删除任何缺失值的地方。幸运的是,这些数据非常干净,甚至没有缺失值。删除后形状保持不变:

size(df)
(31, 4)

size(dropmissing!(copy(df)))
(31, 4)

话虽如此,我们可以直接查看我们的特征。

我们有四个不同的特征,分别是 Index、Girth、Height 和 Volume。Girth、Height 和 Volume 都是连续特征。Index 只是行的表示,是一个标签特征。在某些情况下,可能会混淆我们是否在查看标签或特征。一个很好的方法是查看有多少个唯一值。特别是在这些唯一值的数量与数据长度相同的情况下,这是一个明显的标签指示器。

println(length(Set(df[!, :Index])))
println(length(df[!, :Index]))
31
31

这些数据中没有分类特征,但这给了我们一个很好的机会来创建一个!如前所述,分类特征通常只是对连续特征的测量。例如,我们可以按树的高度对其进行分类。通过一个简单的理解,我将创建一个新的特征,根据均值来确定一个类别:

df[!, Symbol("Height Class")] = [begin
        if x > hmu
            "taller"
        elseif x < hmu
            "shorter"
        else
            "average"
        end
        end for x in df[!, Symbol("Height (ft)")]
     ]

现在我们有了一个新特征!

31-element Vector{String}:
 "shorter"
 "shorter"
 "shorter"
 "shorter"
 "taller"
 "taller"
 "shorter"
 "shorter"
 "taller"
 "shorter"
 "taller"
 "average"
 "average"
 ⋮
 "shorter"
 "taller"
 "taller"
 "shorter"
 "shorter"
 "taller"
 "taller"
 "taller"
 "taller"
 "taller"
 "taller"
 "taller"

现在我们与数据建立了基本关系,我们实际上可以发现一些见解。例如,查看高度是否与宽度相关可能会很有趣。为此,我们现在需要结构化一个测试。

假设检验

现在我们已经建立了特征并识别了数据的格式,我们可以开始制定假设。

根据我们的特征,我们将形成以下假设。

“如果一棵树的高度更高,那么它的胸围也可能更高。”

这是一个有趣的假设,因为答案并不立即显而易见。当然,树木之间存在大量遗传多样性,一些树木长得很高,而一些树木则长得很宽。这些数据也只有 31 个观察值,这可能意味着我们没有足够的普遍性来接受这个假设。不过,仍然可以进行检验!首先,我们将创建我们之前讨论的样本。我们将使用我们的新类别,将树木按照是否较高或较矮进行分类。使用 Julia,我喜欢为此创建一个干净的小调度,使条件掩码变得更加容易。当然,仍然可以使用 filter! 来完成。

import Base: getindex

function getindex(df::DataFrame, bv::Vector{Bool})
    points::Vector{Int64} = findall(x -> x == 0, bv)
    dfcopy::DataFrame = copy(df)
    delete!(dfcopy, points)
    dfcopy::DataFrame
end

现在我将分离出较高的 df:

mask = [x == "taller" for x in df[!, Symbol("Height Class")]]
tallerdf = df[mask]

现在我们可以进行检验,以检查这个新分离样本中的胸径是否具有统计显著性。如果是这样,我们可以接受或拒绝我们的零假设并得到答案。还有更多内容;我们对任何检验都有一定的置信水平,但在这个简要概述中,我们只会略微触及这一点。现在我将拿出我们的两个样本,注意它们需要具有相同的长度,所以我们将随机抽样到我们的样本长度。

grow_samp = tallerdf[!, Symbol("Girth (in)")]
samples = [begin
        n = rand(1:length(grow))
        grow_pop[n] 
    end for n in 1:length(grow_samp) ]

我们的 grow_samp 是我们分类为更高的组。我们的 samples 是来自我们总体的随机样本。利用这两个数据,我们可以确定被标记为较高的树木和未标记的树木在胸径上是否存在差异。为此,我将使用单样本 T 检验,也称为独立 T 检验。这是一种标准的假设检验,可能是最容易学习的。在统计学中,一切都始于抛物线。大多数数据都位于这个抛物线的中心。统计上显著的事物则位于异常区,远离其他数据,位于抛物线的两侧——即尾部。在正态分布的例子中,我们的均值位于中心,两侧各设置两个标准差。在任一方向——均值的负标准差或正标准差。

抛物线本身被称为分布,因为它描述了我们的数据是如何分布的。在大多数情况下,鉴于我们生活在 2023 年,我们很可能会使用已经为你计算了公式的软件。作为初学者,我建议你熟悉概率密度函数(PDFs)和累积分布函数(CDFs),但不一定要学习这些函数的公式。我们将详细介绍一些内容,但不多,这里有一些文章更深入地探讨了这些主题:

## 什么是概率密度函数?

towardsdatascience.com ## 什么是累积分布函数?

CDF 的概述及其在数据科学中的应用

towardsdatascience.com

一个值得熟悉的 PDF 是正态分布的 PDF。这是一个很简单的公式,很有意义。为了激发思考,让我们从后往前推导;我们希望得到什么回报?

我们希望得到一个分布函数,这个函数会根据概率展示我们的数据,其中每个输入都以其与均值的标准差为比例。如果我们从某些数据中取出一个值,我们如何找到它距离均值多少标准差?我们需要 5 个填充花生来运送我们的箱子,我们有 200 个,但用了 125 个,我们还能装多少个箱子?这是一个类似的问题;首先我们得到值和均值之间的差异,然后看到它距离均值有多少标准差,就像我们减去已用花生的数量并除以每箱花生的数量,以查看我们可能装多少个箱子一样。

x̄-µ/σ

其中

  • xbar 是我们样本中的一个观测值,

  • mu 是样本均值,

  • 小写希腊字母 sigma 是标准差。

如果你想了解这些符号在统计学中的含义,这里有一篇我写的文章:

## Python 中的统计希腊字母

熟悉每个希腊字母在统计学中的含义。

towardsdatascience.com

每当我们测试相关性时,我们测试的是我们的数据是否异常;是否落在距离均值两个标准差的尾部。重要的是要准确记住这些测试的工作原理;我们不是在测试因果关系,我们在测试相关性。这并不意味着这些树木因其高度而变宽,它们只是因为更高而变得更宽。另一个需要考虑的因素是我们的假设:

如果一棵树更高,那么它也可能有更大的胸围。

每当我们进行这个测试时,我们并不是在检验某件事是否为真。我们是在检验相反的情况是否为真;我们真正的问题是完全不同的。

如果一棵树更高,那么它的胸围仍然是正常的。

每当我们拒绝否定我们的假设时,我们实际上并不是在拒绝我们最初想要证明的东西;我们只是证明了对立的观点不成立。这被称为零假设,在实验过程中应当时刻记在脑海里。现在回到我们的实验。正如我简要提到的,不同分布的各种函数涉及大量数学。对于不同分布的测试,最易接近的可能是 T 分布,这种分布仍有相当复杂的 CDF,通常用于这种类型的一样本测试。也就是说,我们可以利用软件库和 Julia 生态系统来我们的优势。在像 Python 或 R 这样的语言中,也有类似的包能够执行这种测试。为此,我将使用来自HypothesisTestsOneSampleTTest

OneSampleTTest(samples, grow_samp)

One sample t-test
-----------------
Population details:
    parameter of interest:   Mean
    value under h_0:         0
    point estimate:          -1.24
    95% confidence interval: (-3.338, 0.8581)

Test summary:
    outcome with 95% confidence: fail to reject h_0
    two-sided p-value:           0.2256

Details:
    number of observations:   15
    t-statistic:              -1.2675959523435136
    degrees of freedom:       14
    empirical standard error: 0.9782296935450965

虽然我们在这里可用的观察数据不多,但从这些数据来看,这个假设的统计意义不大。我们发现,相反的情况可能会成立——虽然我们当然需要更多的数据来确定。我们的 P 值仅为 0.225。一般而言,P 值低于 0.05 被认为是具有统计学意义的。我们的值显然表明在这些数据中情况并非如此。

假设检验和应用科学不仅对数据科学家、程序员、分析师等人员具有宝贵的资产,还可以在生活中和理解你周围的世界中发挥重要作用。数学和科学是非常具体和严格的,但同时也常常非常有意义,并让我们通过简单的观察学习新事物。应用科学确实是一种超级力量,而统计在许多不同的情况下具有无限的价值。感谢阅读,希望这篇文章提供了大量有价值的信息!

初学者友好的 MLOps 介绍

原文:towardsdatascience.com/a-beginner-friendly-introduction-to-mlops-95282f25325c

MLOps 的第一步

Chayma ZatoutTowards Data Science Chayma Zatout

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

--

Sarah Dorweiler 拍摄于 Unsplash

我对 MLOps 已经有一段时间的兴趣了。我最初是从机器学习工程师那里了解到它的,当时我还是博士生,不知道它的存在。然而,我的好奇心被激发了,我开始学习它。回顾过去,我后悔没有早点了解它,以便优化我的机器学习工作流。

在这篇文章中,我将尝试提供一个初学者友好的 MLOps 介绍,并用简单的方式解释关键概念。作为一个起初也觉得难以理解的人,我明白需要一个更简单的入门介绍。我的希望是,阅读完这篇文章后,初学者能够更自如地阅读更高级的 MLOps 文档。

目录:

· 1. 对 MLOps 的动机

· 2. 定义

· 3. MLOps 生命周期

· 4. MLOps 工作流

∘ 4.1. 商业问题

∘ 4.2. 数据工程

∘ 4.3. 机器学习模型工程

∘ 4.4. 代码工程

· 5. 结论

我的 MLOps 教程:

[我将在发布相关文章时更新此列表]

1. 对 MLOps 的动机

由于机器学习技术在各种研究领域的成功,许多公司寻求将其纳入他们的软件系统,以提高效率并解决实际问题。然而,在生产环境中实施机器学习对许多公司来说可能是一个具有挑战性且耗时的过程。此外,一旦部署,模型必须被管理和维护,并且其性能必须被监控以确保其正常运作。这些任务在大型软件系统中尤为困难。

另一方面,软件工程师使用 DevOps(开发与运维)范式,这是一套促进开发与运维团队之间协作和沟通的实践和工具,以开发和管理他们的系统。这有助于保持开发速度和质量。MLOps 旨在将这些 DevOps 原则调整为机器学习系统。考虑到这一背景,MLOps 到底是什么呢?

2. 定义

为了定义 MLOps,我们从检查各种定义开始:

定义 1:

“MLOps(机器学习操作)是一个范式,包括最佳实践、概念集合以及在机器学习产品的端到端概念化、实施、监控、部署和可扩展性方面的开发文化。” [1]

定义 2:

“将 DevOps 方法学扩展到包括机器学习和数据科学资产,作为 DevOps 生态系统中的第一类公民” [2]

定义 3:

我们可以使用机器学习工程(MLE)的定义,其中 MLE 是使用科学原则、工具和机器学习及传统软件工程技术来设计和构建复杂计算系统。MLE 包括从数据收集、模型构建到使模型可供产品或消费者使用的所有阶段。”(由 A.Burkov 提供) [3].

基于之前的定义,我们可以理解 MLOps 作为一套技术和实践,用于以高效、优化和有序的方式设计、构建和部署机器学习模型。这些技术和实践通常在 MLOps 生命周期的背景下进行讨论。

3. MLOps 生命周期

MLOps 生命周期 (source) CC BY 4.0

MLOps 生命周期包括 MLOps 范式中的步骤和技术,从设计和开发机器学习模型到在生产环境中部署,并随时间进行监控和维护。它通常分为三个主要阶段:

  • 第一个阶段是设计过程,涉及定义业务问题、模型的要求及其预期用途。这通常涉及创建 AI/ML 画布。

  • 第二个阶段是模型开发过程,包括数据和模型工程。

  • 第三个阶段是运维过程,涵盖了模型的部署和维护。

在模型部署后,维持其性能是很重要的,因此这些阶段通常以循环的方式进行。这确保了模型的良好表现并且仍然满足在第一阶段定义的需求。现在我们已经讨论了 MLOps 生命周期的各个阶段,让我们深入了解 MLOps 工作流程,它概述了在每个阶段所执行的具体任务和活动。

4. MLOps 工作流程

MLOps 工作流程

MLOps 工作流程概述了开发、部署和维护机器学习模型的步骤。在理想的情况下,遵循工作流程就足够了:首先理解业务问题,然后选择、训练和部署模型。然而,现实世界中情况并非总是如此。在任何时候,都可能需要返回到之前的步骤。此外,模型部署后必须进行维护和监控,这就是为什么理解 MLOps 生命周期和 MLOps 工作流程都很重要。

4.1. 业务问题

业务问题(工作流程图、AI 画布和 ML 画布来自来源 CC BY 4.0)

MLOps 工作流程的第一步是理解业务问题,这涉及定义模型的输入和输出,以及过程和其各种子任务。为了构建这一过程,你可以使用 AI(人工智能)画布或 ML(机器学习)画布,这可以看作是组织 MLOps 工作流程的模板。AI 画布通常提供 ML/AI 实现的高层次结构,而 ML 画布提供系统的高层描述和具体细节。你可以在这里阅读更多关于这些画布的信息。

举个例子! 假设为了改善其产品,一家乳制品公司希望收集消费者对其产品的反馈。为此,需要对消费者在社交媒体平台上对产品的评论进行情感分析。可以使用机器学习技术训练模型,将这些评论的情感分类为积极、消极或中性。这将使公司更好地了解客户对其产品的体验,并识别改进的领域。这个业务问题描述转化为 AI 画布和/或 ML 画布,以更清晰地呈现:

  • 预测/预测任务:AI 系统将分析文本输入并预测文本的情感(积极、消极或中性)。

  • 判断:系统将使用自然语言处理技术来理解文本的含义和情感。

  • 行动/决策:根据预测的情感,系统可能会采取不同的措施,例如标记负面评论以便进一步审查,或优先推广积极的社交媒体帖子。

  • 结果:期望的结果是系统能够准确分类文本输入的情感,从而提高客户满意度、改善社交媒体互动或根据具体用例带来其他好处。

  • 训练:系统将使用标注文本数据集进行训练,该数据集包含输入文本及其相应的情感标签。

  • 输入/数据来源:系统将接受来自各种来源的文本输入,例如社交媒体帖子或客户评论。

  • 输出/预测:系统将分析文本输入并预测文本的情感(积极、消极或中性)。

  • 反馈:系统可能会结合用户或利益相关者的反馈,以改善其性能,例如通过调整自然语言处理算法的参数或向训练数据集中添加新数据。

  • 离线评估:系统将使用标准评估指标,如精确度、召回率和 F1 得分进行评估,以确保其准确分类文本输入的情感。

  • 实时监控:系统将会持续监控并根据需要进行更新,以确保其在时间的推移中继续准确地执行。

4.2. 数据工程

数据工程

在了解了当前的业务问题后,MLOps 工作流中的下一步是数据工程过程。这包括数据采集、探索和验证、数据清洗、数据标注和数据拆分。

  • 数据采集涉及使用一系列技术来收集数据、创建备份、保护私人信息、创建元数据目录,并抽样测试集以避免数据窥探偏差。

  • 为了探索和验证数据集,使用了一组统计和可视化技术。

  • 收集到的数据通常存在噪声、包含异常值和缺失值。这些问题可能影响下一步,因此应用数据清洗步骤来解决它们。

  • 数据标注是当选择的模型基于监督学习时所必需的。这一步可以手动、自动或半自动完成。

  • 数据拆分是此过程的最后一步,涉及将数据划分为训练集、验证集和测试集。

4.3. ML 模型工程

ML 模型工程

MLOps 工作流程的第三步是机器学习工程,其中包括模型训练、模型评估、模型测试和模型打包。

  • 训练模型涉及特征工程、代码审查和版本控制,以及超参数调优。你可能会想知道为什么特征工程包含在这一步而不是前一步。原因在于,这一步测试了多种类型和架构的模型,因此特征工程通常并不相同。值得注意的是,在选择最合适的模型之前,会训练和测试多个模型。

  • 模型评估涉及验证模型,以确保它符合在业务问题步骤中描述的业务目标。

  • 模型测试步骤中,使用初始测试集进行模型验收测试。

  • 一旦模型被验证和测试,最后一步是导出模型为特定格式,以便可以提供给业务应用程序。

4.4. 代码工程

代码工程

在这一步,模型已准备好部署到生产环境。模型部署包括三个步骤:模型服务、性能监控和性能日志记录。

  • 为了服务一个模型,必须考虑服务模式和部署策略。服务模式指的是模型如何集成到软件中,例如将其集成作为服务、依赖项、使用预计算服务、按需服务或混合服务。部署策略指的是将模型打包的方式,例如将其部署为 Docker 容器或无服务器函数。

  • 模型监控涉及观察模型的整体行为,例如模型预测与之前模型表现的偏差。

  • 性能日志记录涉及将模型预测的结果保存到日志记录中。

5. 结论

在这篇文章中,我们简要介绍了 MLOps。我们讨论了 MLOps 的必要性,提出了各种定义,解释了 MLOps 生命周期,并描述了 MLOps 工作流程。如果你想了解更多关于 MLOps 的信息,我推荐访问 ml-ops.org

这是关于 MLOps 的第一篇文章,但肯定不是最后一篇!我将继续撰写关于 MLOps 及其各种技术的教程,并附带示例,敬请关注。如果您有任何问题或建议,请随时在下方留言。

参考文献

[1] Kreuzberger, D., Kühl, N., & Hirschl, S. 机器学习操作(mlops):概述、定义和架构,2022. doi: 10.48550. arXiv 预印本 arXiv.2205.02302

[2] MLOps Roadmap 2020

[3] ml-ops.org/content/motivation#mlops-definition

图片来源

本文中所有未在标题中提及来源的图片和图形均由作者提供。

从零开始构建检索增强生成(RAG)应用程序的初学者指南

原文:towardsdatascience.com/a-beginners-guide-to-building-a-retrieval-augmented-generation-rag-application-from-scratch-e52921953a5d?source=collection_archive---------1-----------------------#2023-11-02

用简单易懂的语言学习构建 AI 应用程序的关键知识

Bill ChambersTowards Data Science Bill Chambers

·

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

--

从零开始构建检索增强生成(RAG)应用程序的初学者指南

检索增强生成(RAG)目前非常流行,因为它为像 OpenAI 的 GPT-4 这样的语言模型引入了一些强大的功能——即利用和利用自身数据的能力。

本文将教你 RAG 的基本直觉,并提供一个简单的教程帮助你入门。

在快速变化的领域中学习的问题

AI 领域有太多噪音,特别是关于 RAG 的噪音。供应商们试图使其过于复杂。他们试图注入他们的工具、生态系统和愿景。

这使得 RAG 变得比实际需要的复杂得多。这个教程旨在帮助初学者从零开始学习如何构建 RAG 应用程序。不包含冗余内容,不使用术语(好吧,尽量少用),不依赖库,只是一个简单的逐步 RAG 应用程序。

LlamaIndex 的 Jerry 提倡从零开始构建事物以真正理解各个部分。一旦你做到这一点,使用像 LlamaIndex 这样的库就更有意义了。

从零开始学习,然后用库进行扩展。

让我们开始吧!

介绍我们的概念:检索增强生成

你可能听说过检索增强生成(RAG),也可能没有。

这是Facebook 介绍这个概念的博客文章中的定义:

构建一个能够研究和上下文化的模型更具挑战性,但这是未来发展的关键。我们最近在这一领域取得了重大进展,推出了我们的检索增强生成(RAG)架构,这是一种端到端的可微模型,将信息检索组件(Facebook AI 的密集段落检索系统)与 seq2seq 生成器(我们的双向自回归变换器[BART]模型)相结合。RAG 可以在知识密集型的下游任务上进行微调,与即使是最大的预训练 seq2seq 语言模型相比,能够达到最先进的结果。而且,与这些预训练模型不同,RAG 的内部知识可以随时轻松更改或补充,使研究人员和工程师能够控制 RAG 知道和不知道的内容,而无需浪费时间或计算能力重新训练整个模型。

哇,真是太复杂了。

为了简化技术对初学者,我们可以说 RAG 的本质是将你自己的数据(通过检索工具)添加到你传递给大型语言模型的提示中。这样,你将获得一个输出。这带来了几个好处:

  1. 你可以在提示中包含事实,以帮助 LLM 避免产生幻觉。

  2. 你可以(手动)参考权威来源来回应用户查询,帮助检查潜在问题。

  3. 你可以利用 LLM 可能未曾训练过的数据。

我们 RAG 系统的高级组件

  1. 一组文档(正式称为语料库)

  2. 用户输入

  3. 文档集合与用户输入之间的相似性测量

是的,就是这么简单。

要开始学习和理解基于 RAG 的系统,你不需要一个向量存储,甚至不需要LLM(至少在概念学习和理解方面)。

虽然它经常被描绘得很复杂,但其实不一定如此。

查询 RAG 系统的有序步骤

我们将按顺序执行以下步骤。

  1. 接收用户输入

  2. 执行我们的相似度度量

  3. 对用户输入和获取的文档进行后处理。

后处理是通过 LLM 完成的。

论文本身的备注

实际的 RAG 论文显然是最重要的资源。问题在于它假设了大量的上下文。这比我们需要的要复杂得多。

例如,下面是论文中提出的 RAG 系统概述。

来自 RAG 论文的 RAG 概述 由 Lewis 等人提供

这很密集。

对于研究人员来说,这很棒,但对于我们其他人来说,通过自己构建系统逐步学习将会容易得多。

通过一个示例进行工作——最简单的 RAG 系统

让我们回到从头开始构建 RAG,逐步进行。这里是我们将要经历的简化步骤。虽然这不严格是“RAG”,但它是一个好的简化模型,有助于学习并使我们能够进步到更复杂的变体。

获取文档集合

下面你可以看到我们有一个简单的‘文档’语料库(请宽容一点😉)。

corpus_of_documents = [
    "Take a leisurely walk in the park and enjoy the fresh air.",
    "Visit a local museum and discover something new.",
    "Attend a live music concert and feel the rhythm.",
    "Go for a hike and admire the natural scenery.",
    "Have a picnic with friends and share some laughs.",
    "Explore a new cuisine by dining at an ethnic restaurant.",
    "Take a yoga class and stretch your body and mind.",
    "Join a local sports league and enjoy some friendly competition.",
    "Attend a workshop or lecture on a topic you're interested in.",
    "Visit an amusement park and ride the roller coasters."
]

定义和执行相似度度量

现在我们需要一种方法来衡量我们将要接收的用户输入和我们组织的文档集合之间的相似度。可以说,最简单的相似度度量是杰卡德相似度。我在过去写过这方面的内容(参见这篇文章),但简短的回答是杰卡德相似度是“词集”的交集除以并集。

这使我们能够将用户输入与源文档进行比较。

附注:预处理

一个挑战是,如果我们有一个简单的字符串像"Take a leisurely walk in the park and enjoy the fresh air.",我们需要将其预处理为一个集合,以便进行这些比较。我们将以最简单的方式进行处理,转为小写并按" "分割。

def jaccard_similarity(query, document):
    query = query.lower().split(" ")
    document = document.lower().split(" ")
    intersection = set(query).intersection(set(document))
    union = set(query).union(set(document))
    return len(intersection)/len(union)

现在我们需要定义一个函数,该函数接收精确的查询和我们的语料库,并选择‘最佳’文档返回给用户。

def return_response(query, corpus):
    similarities = []
    for doc in corpus:
        similarity = jaccard_similarity(query, doc)
        similarities.append(similarity)
    return corpus_of_documents[similarities.index(max(similarities))]

现在我们可以运行它了,我们将从一个简单的提示开始。

user_prompt = "What is a leisure activity that you like?"

还有一个简单的用户输入…

user_input = "I like to hike"

现在我们可以返回响应。

return_response(user_input, corpus_of_documents)
'Go for a hike and admire the natural scenery.'

恭喜,你已经构建了一个基本的 RAG 应用程序。

我有 99 个问题,糟糕的相似度是其中之一

现在我们选择了一个简单的相似性度量进行学习。但这会有问题,因为它过于简单。它没有语义的概念。它只是查看两个文档中包含了哪些词。这意味着如果我们提供一个负面例子,我们将得到相同的“结果”,因为这是最接近的文档。

user_input = "I don't like to hike"
return_response(user_input, corpus_of_documents)
'Go for a hike and admire the natural scenery.'

这是一个在“RAG”中经常出现的话题,但目前请放心,我们会在后面解决这个问题。

目前为止,我们还没有对响应的“文档”进行任何后处理。到现在为止,我们只实现了“检索增强生成”的“检索”部分。下一步是通过引入大型语言模型(LLM)来增强生成。

添加 LLM

为此,我们将使用ollama来启动并运行本地机器上的开源 LLM。我们同样可以使用 OpenAI 的 gpt-4 或 Anthropic 的 Claude,但现在我们将从Meta AI的开源 llama2 开始。

本文假设你对大型语言模型有一些基本了解,所以我们直接开始查询这个模型吧。

import requests
import json

首先,我们将定义输入。为了使用这个模型,我们将采取

  1. 用户输入,

  2. 获取最相似的文档(按我们的相似性度量来衡量),

  3. 将其传递给语言模型的提示,

  4. 然后 将结果返回给用户

这引入了一个新术语,即提示。简而言之,它就是你给 LLM 的指令。

当你运行这段代码时,你会看到流式结果。流式处理对用户体验很重要。

user_input = "I like to hike"
relevant_document = return_response(user_input, corpus_of_documents)
full_response = []
prompt = """
You are a bot that makes recommendations for activities. You answer in very short sentences and do not include extra information.
This is the recommended activity: {relevant_document}
The user input is: {user_input}
Compile a recommendation to the user based on the recommended activity and the user input.
"""

确定了这一点之后,现在让我们调用 ollama(和 llama2)的 API。一个重要的步骤是确保 ollama 已经在你的本地机器上运行,通过运行ollama serve

注意:这在你的机器上可能很慢,在我的机器上肯定很慢。请耐心点,小草 hopper。

url = 'http://localhost:11434/api/generate'
data = {
    "model": "llama2",
    "prompt": prompt.format(user_input=user_input, relevant_document=relevant_document)
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(data), headers=headers, stream=True)
try:
    count = 0
    for line in response.iter_lines():
        # filter out keep-alive new lines
        # count += 1
        # if count % 5== 0:
        #     print(decoded_line['response']) # print every fifth token
        if line:
            decoded_line = json.loads(line.decode('utf-8'))

            full_response.append(decoded_line['response'])
finally:
    response.close()
print(''.join(full_response))
Great! Based on your interest in hiking, I recommend trying out the nearby trails for a challenging and rewarding experience with breathtaking views Great! Based on your interest in hiking, I recommend checking out the nearby trails for a fun and challenging adventure.

这给了我们一个完整的 RAG 应用,从零开始,没有提供者,没有服务。你了解了检索增强生成应用中的所有组件。视觉上,这是我们构建的内容。

如果你幸运的话,LLM 将处理与推荐文档不符的用户输入。我们可以在下面看到。

user_input = "I don't like to hike"
relevant_document = return_response(user_input, corpus_of_documents)
# https://github.com/jmorganca/ollama/blob/main/docs/api.md
full_response = []
prompt = """
You are a bot that makes recommendations for activities. You answer in very short sentences and do not include extra information.
This is the recommended activity: {relevant_document}
The user input is: {user_input}
Compile a recommendation to the user based on the recommended activity and the user input.
"""
url = 'http://localhost:11434/api/generate'
data = {
    "model": "llama2",
    "prompt": prompt.format(user_input=user_input, relevant_document=relevant_document)
}
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json.dumps(data), headers=headers, stream=True)
try:
    for line in response.iter_lines():
        # filter out keep-alive new lines
        if line:
            decoded_line = json.loads(line.decode('utf-8'))
            # print(decoded_line['response'])  # uncomment to results, token by token
            full_response.append(decoded_line['response'])
finally:
    response.close()
print(''.join(full_response))
Sure, here is my response:

Try kayaking instead! It's a great way to enjoy nature without having to hike.

改进领域

如果我们回到 RAG 应用的图示,考虑一下我们刚刚构建的内容,我们会看到各种改进的机会。这些机会是工具如向量存储、嵌入和提示“工程”介入的地方。

这里有十个潜在的改进领域:

  1. 文档的数量 👉 更多文档可能意味着更多的推荐。

  2. 文档的深度/大小 👉 更高质量的内容和信息更多的长文档可能更好。

  3. 我们提供给 LLM 的文档数量 👉 目前,我们只给 LLM 一个文档。我们可以将几个文档作为“上下文”输入,并允许模型根据用户输入提供更个性化的推荐。

  4. 我们提供给 LLM 的文档部分 👉 如果我们有较大或更全面的文档,我们可能只想添加这些文档的部分、各个文档的部分,或其某种变体。在词汇中,这被称为分块(chunking)。

  5. 我们的文档存储工具 👉 我们可能以不同的方式或不同的数据库存储文档。特别是,如果我们有大量文档,我们可能会探索将它们存储在数据湖或向量存储中。

  6. 相似度度量 👉 我们如何度量相似度是重要的,我们可能需要在性能和全面性之间进行权衡(例如,查看每一个单独的文档)。

  7. 文档及用户输入的预处理 👉 我们可能在将用户输入传递到相似度度量之前,进行一些额外的预处理或增强。例如,我们可能会使用嵌入将输入转换为向量。

  8. 相似度度量 👉 我们可以更改相似度度量,以获取更好或更相关的文档。

  9. 模型 👉 我们可以更改我们使用的最终模型。我们上面使用的是 llama2,但我们也可以轻松使用 Anthropic 或 Claude 模型。

  10. 提示 👉 我们可以使用不同的提示输入 LLM/模型,并根据我们希望得到的输出进行调整。

  11. 如果你担心有害或有毒的输出 👉 我们可以实现一种“断路器”,运行用户输入以检查是否存在有毒、有害或危险的讨论。例如,在医疗保健背景中,你可以检查信息是否包含不安全的语言,并相应地做出回应——超出典型流程之外。

改进的范围不仅限于这些点;可能性广泛,我们将在未来的教程中深入探讨。在此之前,如果你有任何问题,请随时 在 Twitter 上联系我。祝你 RAGING 愉快 😃。

参考文献

这篇文章最初发布在 learnbybuilding.ai我将在未来几个月举办一个关于如何为产品经理构建生成式 AI 产品的课程, 点击这里报名

构建高质量机器学习数据集的初学者指南

原文:towardsdatascience.com/a-beginners-guide-to-building-high-quality-datasets-for-machine-learning-586a2ce7a565?source=collection_archive---------1-----------------------#2023-11-11

数据清洗、可视化、增强和合成数据生成的工具和技术

Miriam SantosTowards Data Science Miriam Santos

·

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

--

找到数据复杂性的真实来源可能就像在扮演“数据侦探”,直到你找到解锁真正有用洞察的“黄金钥匙”。照片由Michael Dziedzic提供,刊登在Unsplash

智能数据胜过大数据。这是“数据中心人工智能”范式的基本假设。

数据科学家不仅仅是“简单地预处理”数据,还应该建立一种持续且系统的理解和改进数据集的实践。

这将最终将我们的重点从盲目追求通过使用越来越复杂的算法来提升分类结果,转向对分类结果本身的深刻理解,问题的复杂性源自何处,以及如何调整数据以便分类器能够更好地学习问题,从而提高其性能。

如果你是机器学习新手,这可能会让你感到有些畏惧:“构建高质量数据集的最佳实践是什么,如何实施?”

在本教程中,我们将通过一个简单的案例来运用数据中心人工智能范式,以实现高质量数据并改善我们的机器学习分类结果。

根据数据中心人工智能的原则——一切都围绕数据——我们不会深入到模型本身(说实话,它会是一个简单的决策树)。

我们将使用皮马印第安人糖尿病数据集,该数据集在 Kaggle 上免费提供(许可证:CC0: 公开领域)。你还可以在数据中心人工智能社区 GitHub找到所有代码和额外的材料。

我们开始吧?

步骤 1:进行数据概况分析以理解数据

在开始整理我们的数据集之前,我们需要理解我们要解决的问题以及我们处理的数据的特殊性。彻底理解数据特征、问题复杂性和使用案例领域是数据中心人工智能的基本原则之一。

这将帮助我们确定下一步行动,以推进你的机器学习流程。

关于数据概况分析,有几个有趣的开源工具可以探索:我自己做了一些评测,包括[ydata-profiling](https://github.com/ydataai/ydata-profiling)[dataprep](https://github.com/sfu-db/dataprep)[sweetviz](https://github.com/fbdesignpro/sweetviz)[autoviz](https://github.com/AutoViML/AutoViz)[lux](https://github.com/lux-org/lux)

我目前主要使用ydata-profiling:我发现它是一个顶尖的工具,可以让数据从业者在几行代码中完成对数据特征和可视化的全面分析,而无需费劲地使用 pandas。

首先,你需要安装 ydata-profiling(最好使用虚拟环境——如果你不知道怎么做,你可以 查看这个 2 分钟的视频,或者如果你从未在 conda 环境中工作过,可以查看这个 完整教程):

然后,我们可以通过保存一个.html报告来获得数据的完整概述,其中包含所有必要的特征和可视化:

数据报告立即让我们了解数据的整体特征,并突出一些我们可能需要考虑的警告:

YData Profiling 报告:查找数据集的基本统计信息、可视化和质量警告。

数据集包含 768 个观察值和 9 个变量/特征。虽然 8 个是数值型的,1 个被识别为分类变量(Outcome似乎是我们的目标)。没有重复的行,并且显然没有缺失值。最后,特征中发现了一些高相关性警告。此外,几个特征有大量的零值

现在是时候充当数据侦探了。高相关性在生物特征中是比较常见的,但这些值怎么样呢?

查看一些突出的特征(例如BMI),我们可以看到这些值与整体分布相差甚远。根据领域知识,这些“0”值实际上是没有意义的:怀孕次数的 0 值是可以接受的,但 BMI、葡萄糖、胰岛素、血压或皮肤厚度的 0 值则是无效的。

YData Profiling 报告:BMI 特征,表明零值与分布有很大差距。

我们很快意识到这些零值编码了什么:缺失数据

现在我们将解决这个问题,但一个彻底的 EDA 过程可以涵盖更多内容。查看这个 探索性数据分析的基本指南 以了解你可以从数据中发现什么其他信息。

第 2 步:调查数据质量问题

既然我们发现某些列有无效的零值,我们可以开始处理数据集中的缺失数据问题。

许多机器学习模型和 scikit-learn 估算器本身不支持缺失值,因此在将数据集提供给估算器之前,我们需要以某种方式处理这些 NaNs。

首先,我们将这些 0 值标记为 NaN 值:

现在,我们可以使用数据填充来用合理的替代值替换 NaN 观察值。

“没有免费午餐”定理告诉我们,没有一种最佳的解决方案适用于所有情况——我们应该调查不同解决方案对训练数据复杂性的影响,并确定什么能最好地提升我们的机器学习模型。这实际上是数据中心 AI 的另一个原则:不断迭代和改进

目前我们将使用一个非常简单的方法——SimpleImputer——将零值替换为每个特征的均值。这是一个非常初级的方法,可能会在我们的分布中产生一些不希望有的“尖峰”,但目标只是展示如何突出和填补缺失数据,我们可以稍后尝试更好的方法:

现在,我们可以尝试一个非常简单的决策树分类器,看看我们的分类结果的基线是什么。作为旁注,决策树可以扩展以自然支持缺失值,通过替代拆分或其他方法。确实,在 scikit-learn 的文档 中似乎决策树在当前版本 (1.3.2) 中确实支持缺失值。然而,当我使用版本 1.2.2 时,我遇到了这个错误:

即使 NaN 值在内部处理了,用缺失数据训练你的模型也不是一个好习惯,因为这会危及模型从混乱和有限的信息中学习到的概念。

这是混淆矩阵:

分类结果不是很好。请记住,我们使用的是简单决策树,但仍然……我们目标类别的预测存在显著差异。为什么分类器在类别“0”上的表现优于类别“1”?

第 3 步:增强不足代表的类别

如果我们在第 1 步中有留意到(也许你已经发现了),我们的目标类别Outcome是不平衡的。虽然可能不足以在默认设置中触发警告(默认阈值0.5),但足以使分类器对多数类别产生偏向,忽视少数类别。从 Profiling Report 中提供的数据可视化中可以清楚地看出这一点:

YData Profiling Report: 结果类别“0”和“1”并不均等地代表。分类器自然会对代表性较强的类别“0”更有偏向,忽视类别“1”。

请注意,虽然缺失数据可能是由于数据收集、传输或存储过程中出现的多个错误造成的,类不平衡可能反映了领域的自然特征:例如,这个医疗中心诊断出的糖尿病患者较少。

然而,对训练数据进行处理仍然很重要,以确保模型不会忽视少数类别:事实上,这就是我们试图更准确预测的内容。

假阳性是糟糕的,因为它会给健康患者错误的信息,告诉她她有糖尿病。 但当进行额外的测试时,这将只是一个“惊吓”。

然而,在这种情况下,假阴性更糟。 我们将告诉一位患有糖尿病的患者一切正常,她未被诊断出来,疾病继续发展。

增加这些数字的一种方法是使用数据过采样技术。 数据过采样是数据从业者中流行的一种技术,用于调整数据集中现有类别或分类之间的比例,从而缓解数据不平衡问题。

这只是合成数据的许多有趣和有用应用之一。

虽然合成数据可能有多种解释 — 例如,“假数据”,“虚拟数据”,“模拟数据” — 但在这里我们指的是“数据驱动的”合成数据生成。

从这个意义上说,合成数据是人工生成的,保留了真实数据的特征 — 结构、统计属性、依赖关系和相关性。

有大量的方法和开源工具可用于生成合成数据 — [ydata-synthetic](https://github.com/ydataai/ydata-synthetic), [sdv](https://github.com/sdv-dev/SDV), [gretel-synthetics](https://github.com/gretelai/gretel-synthetics), [nbsynthetic](https://github.com/NextBrain-ai/nbsynthetic), 和 [synthcity](https://github.com/vanderschaarlab/synthcity) 只是我过去尝试过的一些。

同样地…… 这里没有“免费午餐”:选择最合适的方法将始终取决于需要合成数据的目标。

为了快速掌握合成数据如何用于增强,我们将利用[ydata-synthetic](https://github.com/ydataai/ydata-synthetic)包,并尝试他们的高斯混合模型

首先,我们需要安装该包:

一旦完成这些步骤,创建合成数据就非常简单了:

获得我们的合成数据之后,我们可以简单地从合成数据中抽取新生成的少数类样本的子集,并将其添加到训练数据中,以创建一个平衡(即 50%-50%)的分布:

看看这如何影响我们决策树的学习及其随后的结果:

以及混淆矩阵:

注意到我们对训练集进行如此简单的修改,导致我们的 F-score 提升了 10%,少数类敏感性结果显著改善(从 53%提高到 73%)。

这就是数据中心化 AI 范式的美妙之处:在不触及模型参数化的情况下,我们用非常简单的启发式方法和标准技术显著改善了我们的训练集的质量 — 想象一下如果我们采用更先进的策略和专门的数据准备流水线能做些什么!

当然,类别 0 的召回率有些下降,但最终,我们需要这个模型比特异性更敏感(即更好地检测正类而非负类),这是由于我们所面临的特定约束:疾病诊断——再一次,数据中心人工智能的另一个原则:方法和结果需要根据领域的需求和约束进行评估。

最终思考和进一步方向

在本文中,我们用一个非常实际的案例实验了数据中心人工智能范式。

我们总是从理解数据开始。我们发现、调查并解决了特定的数据质量问题,如缺失数据以及通过合成数据来改善我们的训练数据,以克服领域的不平衡性。当然,对于这样一个快速而简单的案例研究,我们专注于简单的启发式方法来完成工作,但数据科学家的工作从未止步于此。

如果我们考虑不同的插补方法,结果会如何变化?我们如何在合成数据生成中获得更好的拟合?我们是否应该平衡两个类别,还是提高少数类别的表示?某些特征转换或降维是否有助于分类结果?我们是否应该去除一些混淆特征?

所有这些问题在任何机器学习项目开始时似乎都是不可知的。但是随着我们开始测量和揭示每个数据集中的复杂源,我们可以获得更好的见解,了解哪些方法可以改进分类结果(一种“元学习”方法)。当然,数据需要根据数据特征和项目的最终目标进行处理和改进。

生产一个预定义的流程并将数据准备视为一刀切的解决方案,就像是盲目飞行一样。相反,一个熟练的数据科学家会不断扮演数据侦探的角色,并尝试根据数据留给我们的线索找到最佳的技术。通常这确实有效。我们只需要保持警觉!

我希望你喜欢这个教程,像往常一样,反馈、问题和建议非常受欢迎。请在评论中告诉我你希望我写关于哪些其他主题!

关于我

博士,机器学习研究员,教育者,数据倡导者,以及全能型人才。在 Medium 上,我撰写关于数据中心人工智能和数据质量的文章,教育数据科学和机器学习社区如何将不完美的数据转变为智能数据。

数据中心人工智能社区 | GitHub | Google Scholar | LinkedIn

《线性规划及单纯形算法初学者指南》

原文:towardsdatascience.com/a-beginners-guide-to-linear-programming-and-the-simplex-algorithm-87db017e92b4

玉米农场。Dall-E 2 的静物画。

解决各种优化问题

Hennie de HarderTowards Data Science Hennie de Harder

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

--

线性规划是一种强大的工具。它在工程、金融和运筹学等广泛领域中都有应用。线性规划已被用来解决从航空公司航班调度到制造过程设计等各种问题。在这篇博客文章中,我们将探讨线性规划的基础知识以及它如何用来解决实际问题。

线性规划(LP)是一种数学优化技术。它用于寻找涉及多个变量和约束条件问题的最佳解决方案。通过将问题的约束条件和目标函数表示为线性方程和不等式的系统,线性规划算法可以系统地搜索解空间,找到最大化或最小化给定目标的解决方案。

首先,我会给出一个简单的例子。然后我们将深入探讨单纯形算法,它在后台用于快速找到 LP 问题的最佳解决方案。

线性规划问题的示例

假设一个农民拥有 120 英亩的土地用来种植小麦和玉米。小麦每吨需要 2 英亩土地,玉米每吨需要 1 英亩土地。农民希望最大化作物的利润,小麦每吨€100,玉米每吨€150。农民还有限制,每种小麦的产量不超过 50 吨,每种玉米的产量不超过 40 吨。

要使用线性规划解决这个问题,我们首先需要确定决策变量,即农民将种植的小麦和玉米的数量。我们将使用变量 wc 分别表示小麦和玉米的吨数。

接下来,我们需要编写目标函数,它表示农民想要最大化的利润。在这种情况下,目标函数是:

然后,我们需要编写约束条件,这些条件代表了可以生产的小麦和玉米的数量限制。约束条件如下:

前两个约束条件表示可以生产的小麦和玉米总吨数的限制。第三个约束条件表示可用土地总面积的限制。

为了解决这个问题,我们可以使用简单形算法或其他线性规划方法来寻找最大化目标函数的* w* 和 * c* 的值,同时满足约束条件。在这种情况下,最优解是种植 40 吨小麦和 40 吨玉米,这样可以获得€10,000 的利润。

这只是一个简单的例子,用来说明解决线性规划问题的基本步骤。在实际应用中,线性规划用于解决更多变量和约束条件的复杂问题。

下面是问题的可视化:

线性规划可视化。图片由作者提供。

灰色区域称为可行区域。区域中的每一个点都包含一个可行解。该区域受约束条件的限制。

线性规划基本定理 说明了最大值出现在区域的角落。这是简单形算法使用的重要信息。

简单形算法

简单形算法是一种广泛使用的解决线性规划问题的方法。它由乔治·丹茨格于 1947 年开发。该算法的直观思路是以系统的方式从可行区域的角落“行走”。当找到最优解时,过程结束。

简单形算法是一个迭代过程,依靠数学计算和逻辑推理来找到线性规划问题的最优解。它高效可靠,也用于混合整数规划(在约束条件放松后)。简单形算法的基本步骤如下:

  1. 将线性规划问题的约束条件转化为线性方程组的标准形式。

  2. 通过将一些变量设置为零,并求解其余变量,找到一个基本可行解。

  3. 测试基本可行解是否最优。如果是,算法结束。如果不是,算法选择一个变量进入基准,并相应更新基本可行解。

  4. 重复步骤 3,直到找到最优解。

让我们逐步走过小麦和玉米的例子。

步骤 1. 将线性规划问题的约束转换为线性方程组,即标准形式。 第一步是将约束和目标重写为标准形式:

重写为标准形式。图片由作者提供。

在标准形式中,“小于”符号被替换为等号,并且每个约束都有一个松弛变量添加(s1、s2、s3)。这样,小于仍然得以保持,因为松弛变量只能取得正值。

通常,表格用于使问题更具可读性。表格是一个我们写下系数的表格。列代表一个变量,行代表一个约束。在这种情况下,底部行包含目标函数的系数。最后一列包含方程右侧的值。我们使用标准形式来创建表格:

玉米和小麦问题的表格。每个约束和目标都在行中表示,而列表示变量。值是系数。图片由作者提供。

步骤 2. 通过将某些变量设置为零并求解其余变量来找到一个基本可行解。 现在我们有了表格,我们可以使用它来找到一个基本可行解。基本可行解是一个满足所有约束并位于一个顶点上的解。我们可以很容易地从表格中检测到一个。

在表格中,有两种类型的变量:基本变量非基本变量。基本变量只有一个非零项,而非基本变量有多个非零项。基变量 包含所有基本变量。如果我们查看当前的表格状态,松弛变量 s₁s₂s₃ 在基变量中。变量 w 和 c 是非基本的,并被设置为零。当前基本可行解的值为:s₁ = 50,s₂ = 40,s₃ = 120,Z = 0,w = 0 和 c = 0。

非基本变量和基本变量。图片由作者提供。

该解对应于以下角点:

单纯形算法的起点。图片由作者提供。

步骤 3. 测试基本可行解是否最优。如果是,则算法结束。如果不是,算法选择一个变量进入基变量,并相应地更新基本可行解。 通过查看表格中的目标行,可以测试基本可行解是否最优。由于这是一个最大化问题,我们可以通过去除负系数来改进它。如果所有负值都消失了,我们就无法再改进了。底部行中存在负值,因此可以改进,我们尚未找到最优解。

根据目标行中的负值选择进入变量。图像由作者提供。

现在,我们需要选择一个变量进入基。选择进入变量时,取目标行中最低的系数。在我们的例子中,它是 -150,对应于 c。这就是进入变量。接着,我们需要选择要进行高斯消元的行。这是通过将右侧值除以 c 的系数来完成的。因此,在我们的例子中,第二行得到 40/1=40,第三行得到 120/1=120。最低值是 40,这就是为什么使用第二行。现在我们可以用第 2 行清除表格,以将 c 包括在基中:

执行高斯消元以使变量c进入基:从第 3 行中减去第 2 行,将第 2 行乘以 150 后加到第 4 行。图像由作者提供。

新的目标值是 6000,基中的变量是 cs₁s₃,分别为 40、50 和 80。我们移动到一个新的角点,即下一个基本可行解:

现在我们转到下一个可行解(右下角)。图像由作者提供。

第 4 步。重复第 3 步,直到找到最优解。 由于表格的目标行中有负值,因此未找到最优解。所以让我们重复第 3 步:进入变量是 w,因为它在目标行中有最低的系数。我们应该使用第 3 行进行高斯消元,因为 80/2 < 50/1。经过行简化,表格如下:

w 进入基,将第 3 行除以 2,从第 1 行中减去这一新行 3,将第 3 行乘以 100 后加到第 4 行。图像由作者提供。

基中的变量是 wcs₁,对应的值分别为 40、40 和 10。Z(目标)的值等于 10000。这正是我们一开始得到的最优解。底行中没有负值,所以这是最优解。我们采取的步骤:

在使用单纯形算法解决问题时所采取的步骤。从左下角开始,走两步到达最优解。图像由作者提供。

这是一个简单的单纯形算法工作原理的例子。它用于解决比我们讨论的更复杂的问题。

结论

线性规划是一种工具,可以帮助个人和组织充分利用资源,实现目标。无论你是试图最大化利润的企业主、寻求优化复杂过程的研究员,还是学习优化技术的学生,线性规划都提供了广泛的应用和成长机会。通过理解线性规划的原理和单纯形算法,你可以迈出解决各种优化问题的第一步,并做出能够产生持久影响的明智决策。

在实践中应用这些技术时,你不需要自己实现算法,因为优化软件会为你完成这项工作。

单纯形算法也应用于混合整数规划,其中约束的放松使得算法得以应用。如果你想了解更多,可以阅读下面的文章。

## 如何处理优化问题?

简单示例及其解决方案和代码。

[towardsdatascience.com ## 数据科学家应该知道的数学优化启发式方法

局部搜索、遗传算法等。

[towardsdatascience.com

LLM 微调的初学者指南

原文:towardsdatascience.com/a-beginners-guide-to-llm-fine-tuning-4bae7d4da672

如何使用一个工具微调 Llama 及其他 LLM

Maxime LabonneTowards Data Science Maxime Labonne

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

--

图片由作者提供

对大型语言模型(LLMs)的日益关注导致了旨在简化其训练过程的工具和封装的激增

流行的选项包括 LMSYS 的FastChat(用于训练Vicuna)和 Hugging Face 的transformers/trl库(用于我之前的文章)。此外,每个大型 LLM 项目,如WizardLM,通常都有自己独特的训练脚本,灵感来自于最初的Alpaca实现。

在这篇文章中,我们将使用由 OpenAccess AI Collective 创建的Axolotl工具。我们将使用它在一个包含 1,000 个 Python 代码样本的 evol-instruct 数据集上微调一个Code Llama 7b模型。

🤔 为什么选择 Axolotl?

Axolotl 的主要吸引力在于它提供了一站式解决方案,其中包括众多功能、模型架构和一个活跃的社区。以下是我最喜欢的几点:

  • 配置:用于训练 LLM 的所有参数都整齐地存储在 yaml 配置文件中。这使得模型的共享和再现变得方便。你可以在这里查看 Llama 2 的示例。

  • 数据集灵活性:Axolotl 允许指定多个数据集,支持各种提示格式,如 alpaca({"instruction": "...", "input": "...", "output": "..."}),sharegpt:chat({"conversations": [{"from": "...", "value": "..."}]})和原始完成({"text": "..."})。组合数据集非常顺畅,并且避免了统一提示格式的麻烦。

  • 功能:Axolotl 配备了 SOTA 技术,如 FSDP、deepspeed、LoRA、QLoRA、ReLoRA、样本打包、GPTQ、FlashAttention、xformers 和 rope scaling。

  • 工具:集成了众多用户友好的工具,包括添加或更改特殊 token,或自定义 wandb 配置。

一些使用此工具训练的知名模型包括 OpenAccess AI Collective 的Manticore-13b和 Eric Hartford 的Samantha-1.11–70b。像其他包装器一样,它建立在 transformers 库之上,并使用了许多其功能。

⚙️ 创建你自己的配置文件

在开始之前,我们需要一个配置文件。你可以重用[examples](https://github.com/OpenAccess-AI-Collective/axolotl/tree/main/examples)文件夹中的现有配置。我们将调整QLoRA 配置以创建我们自己的Code Llama模型。该模型将在[nickrosh/Evol-Instruct-Code-80k-v1](https://huggingface.co/datasets/nickrosh/Evol-Instruct-Code-80k-v1)数据集中的 1,000 个 Python 样本子集上进行训练。

首先,我们必须将base_modelbase_model_config字段更改为"codellama/CodeLlama-7b-hf"。为了将我们训练的适配器推送到 Hugging Face Hub,我们需要添加一个新的字段hub_model_id,对应我们的模型名称"EvolCodeLlama-7b"。现在,我们必须将数据集更新为[mlabonne/Evol-Instruct-Python-1k](https://huggingface.co/datasets/mlabonne/Evol-Instruct-Python-1k)并将type设置为"alpaca"。

数据集中没有大于 2048 个 token 的样本,因此我们可以将sequence_len减少到"2048"以节省一些 VRAM。说到 VRAM,我们将使用micro_batch_size为 10 和gradient_accumulation_steps为 1,以最大化其使用。在实际操作中,你可以尝试不同的值,直到使用超过 95%的可用 VRAM。

为了方便,我将"axolotl"添加到wandb_project字段中,以便在我的账户上更容易追踪。我还将warmup_steps设置为"100"(个人偏好),将eval_steps设置为 0.01,以便进行 100 次评估。

最终的配置文件应该是这样的:

base_model: codellama/CodeLlama-7b-hf
base_model_config: codellama/CodeLlama-7b-hf
model_type: LlamaForCausalLM
tokenizer_type: LlamaTokenizer
is_llama_derived_model: true
hub_model_id: EvolCodeLlama-7b

load_in_8bit: false
load_in_4bit: true
strict: false

datasets:
  - path: mlabonne/Evol-Instruct-Python-1k
    type: alpaca
dataset_prepared_path: last_run_prepared
val_set_size: 0.02
output_dir: ./qlora-out

adapter: qlora
lora_model_dir:

sequence_len: 2048
sample_packing: true

lora_r: 32
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules:
lora_target_linear: true
lora_fan_in_fan_out:

wandb_project: axolotl
wandb_entity:
wandb_watch:
wandb_run_id:
wandb_log_model:

gradient_accumulation_steps: 1
micro_batch_size: 10
num_epochs: 3
optimizer: paged_adamw_32bit
lr_scheduler: cosine
learning_rate: 0.0002

train_on_inputs: false
group_by_length: false
bf16: true
fp16: false
tf32: false

gradient_checkpointing: true
early_stopping_patience:
resume_from_checkpoint:
local_rank:
logging_steps: 1
xformers_attention:
flash_attention: true

warmup_steps: 100
eval_steps: 0.01
save_strategy: epoch
save_steps:
debug:
deepspeed:
weight_decay: 0.0
fsdp:
fsdp_config:
special_tokens:
  bos_token: "<s>"
  eos_token: "</s>"
  unk_token: "<unk>"

你也可以在这里找到这个配置文件,作为一个 GitHub gist。

在开始训练模型之前,我想介绍几个重要的参数:

  • QLoRA:我们使用 QLoRA 进行微调,这就是为什么我们以 4 位精度(NF4 格式)加载基础模型。你可以查看这篇文章来了解更多关于 QLoRA 的内容,作者是本杰明·玛丽

  • 梯度检查点:通过删除在反向传播过程中按需重新计算的一些激活来降低 VRAM 需求。根据 Hugging Face 的文档,它还会使训练速度降低约 20%。

  • FlashAttention:这实现了FlashAttention机制,通过巧妙融合 GPU 操作来提高模型的速度和内存效率(了解更多内容,请参阅这篇文章,作者是Aleksa Gordić)。

  • 样本打包:一种通过重新组织样本顺序来创建尽可能少填充的批次的智能方法(装箱问题)。因此,我们需要更少的批次来训练相同的数据集。它的灵感来自于Multipack Sampler(见我的笔记)和Krell 等人

你可以在其他一些工具中找到 FlashAttention,但样本打包相对较新。据我所知,OpenChat是第一个在微调过程中使用样本打包的项目。感谢 Axolotl,我们可以免费使用这些技术。

🦙 微调 Code Llama

配置文件准备好之后,就该开始实际的微调工作了。你可以考虑在 Colab 笔记本上运行训练。然而,对于那些没有高性能 GPU 的人来说,一个更具成本效益的解决方案是租用基于云的 GPU 服务,如 AWS、Lambda LabsVast.aiBananaRunPod

就我个人而言,我使用 RunPod,这是微调社区中一个受欢迎的选项。它不是最便宜的服务,但在界面清晰度和成本之间达到了一个良好的折中。你可以使用你喜欢的服务轻松复制以下步骤。

当你的 RunPod 账户设置好后,转到“管理”>“模板”并点击“新建模板”。以下是一个简单的模板:

作者提供的图像

让我们回顾一下不同的字段及其对应的值:

  • 模板名称:Axolotl(你可以选择任何你想要的名称)

  • 容器镜像: winglian/axolotl-runpod:main-py3.10-cu118–2.0.1

  • 容器磁盘: 100 GB

  • 存储卷: 0 GB

  • 卷挂载路径: /workspace

此外,还有两个有用的环境变量可以包含:

  • HUGGING_FACE_HUB_TOKEN: 你可以在 此页面 上找到你的令牌(需要账户)

  • WANDB_API_KEY: 你可以在 此页面 上找到你的密钥(需要账户)

另外,你可以稍后在终端登录(使用 huggingface-cli login 和 wandb login)。设置完成后,前往 Community Cloud 并部署 RTX 3090。你可以在这里搜索你的模板名称并选择它,如下所示:

图片由作者提供

你可以点击“继续”,RunPod 将部署你的模板。你可以在你的 pod 的日志中查看安装情况(管理 > Pods)。当选项可用时,点击“连接”。在这里,点击“启动 Web 终端”,然后“连接到 Web 终端”。你现在已连接到你的 pod!

以下步骤是无论你选择哪个服务都相同的:

  1. 我们按如下方式安装 Axolotl 和 PEFT 库:
git clone https://github.com/OpenAccess-AI-Collective/axolotl
cd axolotl

pip3 install -e .[flash-attn]
pip3 install -U git+https://github.com/huggingface/peft.git

2. 下载我们创建的配置文件:

wget https://gist.githubusercontent.com/mlabonne/8055f6335e2b85f082c8c75561321a66/raw/93915a9563fcfff8df9a81fc0cdbf63894465922/EvolCodeLlama-7b.yaml

3. 你现在可以使用以下命令 开始微调模型

accelerate launch scripts/finetune.py EvolCodeLlama-7b.yaml

如果一切配置正确,你应该能够在 一个多小时 内训练模型(我花了 1 小时 11 分钟 44 秒)。如果你检查使用的 GPU 内存,你会发现几乎达到 100%,这意味着我们正在很好地优化它。如果你使用具有更多 VRAM 的 GPU(如 A100),你可以增加微批量大小以确保充分利用它。

与此同时,可以关闭 Web 终端,并在 Weights & Biases 上检查你的损失。我们使用 tmux,所以即使你关闭终端,训练也不会停止。以下是我的损失曲线:

图片由作者提供

我们看到评估损失稳步改善,这是一个好兆头。然而,你也可以发现评估损失的下降并未与输出质量的降低相关联… 评估你的模型的最佳方式是直接使用它:你可以在终端中运行命令 accelerate launch scripts/finetune.py EvolCodeLlama-7b.yaml --inference --lora_model_dir="./qlora-out"

QLoRA 适配器应该已经上传到 Hugging Face Hub。然而,你也可以通过以下步骤将基础 Code Llama 模型与此适配器合并并将合并后的模型推送到那里:

  1. 下载 这个脚本
wget https://gist.githubusercontent.com/mlabonne/a3542b0519708b8871d0703c938bba9f/raw/60abc5afc07f9d843bc23d56f4e0b7ab072c4a62/merge_peft.py

2. 使用以下命令执行:

python merge_peft.py --base_model=codellama/CodeLlama-7b-hf --peft_model=./qlora-out --hub_id=EvolCodeLlama-7b

恭喜,你现在应该在 Hugging Face Hub 上拥有你自己的 EvolCodeLlama-7b!作为参考,你可以在这里访问我用这个过程训练的模型: [mlabonne/EvolCodeLlama-7b](https://huggingface.co/mlabonne/EvolCodeLlama-7b)

考虑到我们的 EvolCodeLlama-7b 是一个代码 LLM,比较它在标准基准上的表现与其他模型会很有趣,例如HumanEvalMBPP。作为参考,你可以在以下地址找到排行榜:多语言代码评估

如果你对这个模型满意,你可以使用 GGML 进行量化以便本地推理,使用这个免费的 Google Colab 笔记本。你还可以利用deepspeed更大的模型(例如,70b 参数)进行微调,只需额外的配置文件即可。

结论

在本文中,我们覆盖了如何高效微调 LLMs的基本要点。我们自定义了参数,在一个小的 Python 数据集上训练了我们的 Code Llama 模型。最后,我们合并了权重,并将结果上传到 Hugging Face。

我希望你觉得这份指南有用。我推荐使用 Axolotl 与基于云的 GPU 服务来获取一些经验,并在 Hugging Face 上上传几个模型。构建自己的数据集,调整参数,过程中可能会遇到一些问题。就像使用任何包装器一样,不要犹豫去查看源代码,以便对其实际操作有一个很好的直观了解。这样在长期内会大大受益。

感谢 OpenAccess AI Collective 和所有贡献者!

如果你对 LLMs 的更多技术内容感兴趣,在 Medium 上关注我

相关文章

在 Colab 笔记本中微调你自己的 Llama 2 模型

LLM 微调的实用介绍

4 位量化与 GPTQ

使用 AutoGPTQ 对自己的 LLMs 进行量化

4 位量化与 GPTQ

了解更多关于机器学习的内容,并通过一键支持我的工作——在这里成为 Medium 会员:

加入 Medium,使用我的推荐链接 - Maxime Labonne

作为 Medium 会员,你的会员费用的一部分会给你阅读的作者,同时你可以全面访问每个故事…

medium.com

初学者指南:通过蒙特卡罗模拟理解 A/B 测试性能

原文:towardsdatascience.com/a-beginners-guide-to-understanding-a-b-test-performance-through-monte-carlo-simulations-6b1155315376

Ida Johnsson, PhDTowards Data Science Ida Johnsson, PhD

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

--

本教程探讨了协变量如何在随机实验中影响 A/B 测试的精度。一个适当随机化的 A/B 测试通过比较治疗组和对照组的平均结果来计算提升。然而,除了治疗以外的特征对结果的影响决定了 A/B 测试的统计特性。例如,在测试提升计算中遗漏重要特征可能导致提升估计的不精确,即使随着样本量的增加,它也会收敛到真实值。

你将学习 RMSE、偏差和测试规模是什么,并通过生成模拟数据和运行蒙特卡罗实验来理解 A/B 测试的性能。这种工作有助于了解数据生成过程(DGP)的特性如何影响 A/B 测试的性能,并将帮助你在实际数据上进行 A/B 测试。首先,我们讨论估计量的一些基本统计特性。

估计量的统计特性

均方根误差(RMSE)

RMSE(均方根误差):RMSE 是一个经常使用的衡量模型或估计量预测值与观测值之间差异的指标。它是预测值与实际观测值之间平均平方差异的平方根。RMSE 的公式是:

RMSE = sqrt[(1/n) * Σ(actual - prediction)²]

RMSE 对大误差给予相对较高的权重,因为在平均之前这些误差会被平方,这意味着当大误差不受欢迎时,RMSE 更为有用。

偏差

在统计学中,估计量的偏差是该估计量的期望值与被估计参数的真实值之间的差异。一个具有零偏差的估计量被称为无偏的;否则,该估计量被称为有偏的。换句话说,当算法因未能看到准确的潜在关系而始终学习到相同的错误内容时,就会发生偏差。

例如,如果你尝试根据房子的特征预测房价,而你的预测始终比实际价格低$100,000,那么你的模型是有偏的。

规模

在统计学中的假设检验中,“检验规模”指的是检验的显著性水平,通常用希腊字母α(alpha)表示。显著性水平或检验规模是检验统计量必须超过的阈值,以拒绝假设。

它表示在零假设实际上为真的情况下,拒绝零假设的概率,这是一种被称为 I 型错误或假阳性的错误。

例如,如果一个检验的显著性水平设为 5%(α = 0.05),这意味着在零假设为真的情况下,有 5%的风险会拒绝零假设。这个水平 0.05 是α的常见选择,尽管其他水平,如 0.01 或 0.10,也可以根据背景和研究领域使用。

检验规模越小,拒绝零假设所需的证据就越强,从而降低了 I 型错误的可能性,但可能会增加 II 型错误的机会(当零假设为假时未能拒绝它)。I 型错误和 II 型错误之间的平衡是设计任何统计检验时的重要考虑因素。

实证规模

在通过蒙特卡洛模拟进行的假设检验中,实证规模指的是在零假设为真的情况下,零假设在模拟中被错误拒绝的比例。这本质上是 I 型错误率的模拟版本。

下面是一般的操作流程:

1. 设置你的零假设并选择一个显著性水平(例如,α = 0.05)。

2. 在假设零假设为真的情况下生成大量样本。样本的数量通常非常大,如 10,000 或 100,000,以确保结果的稳定性。

对于每个样本,进行假设检验并记录零假设是否被拒绝(你可以将拒绝记录为 1,未拒绝记录为 0)。

4. 计算实证规模,即在零假设被拒绝的模拟比例。这估计了在给定检验程序下,零假设为真的情况下拒绝零假设的概率。

下面的代码显示了如何实现这一点。

import numpy as np
from scipy.stats import ttest_1samp
import random

random.seed(10)

def calculate_empirical_size(num_simulations: int, sample_size: int, true_mean: float, significance_level: float) -> float:
    """
    Simulates a set of samples and conducts a hypothesis test on each, then calculates the empirical size.

    Parameters:
    num_simulations (int): The number of simulations to run.
    sample_size (int): The size of each simulated sample.
    true_mean (float): The true mean under the null hypothesis.
    significance_level (float): The significance level for the hypothesis tests.

    Returns:
    float: The empirical size, or the proportion of tests where the null hypothesis was rejected.
    """
    import numpy as np
    from scipy.stats import ttest_1samp

    # Initialize counter for null hypothesis rejections
    rejections = 0

    # Run simulations
    np.random.seed(0)  # for reproducibility
    for _ in range(num_simulations):
        sample = np.random.normal(loc=true_mean, scale=1, size=sample_size)
        t_stat, p_value = ttest_1samp(sample, popmean=true_mean)
        if p_value < significance_level:
            rejections += 1

    # Calculate empirical size
    empirical_size = rejections / num_simulations

    return empirical_size

calculate_empirical_size(1000, 1000, 0, 0.05)

对于每个 1000 次模拟,从均值为 0 和标准差为 1 的正态分布中抽取一个大小为 1000 的随机样本。进行一次样本 t 检验,以测试样本均值是否显著不同于真实均值(在这种情况下为 0)。如果测试的 p 值小于显著性水平(0.05),则拒绝原假设。

经验规模计算为原假设被拒绝的次数(假阳性次数)除以模拟的总次数。这个值应该在一个良好校准的测试中接近名义显著性水平。在这种情况下,该函数返回经验规模,给出在实际应用中在假设与模拟条件相同的情况下,你可能错误拒绝原假设的频率。

由于随机变异,经验规模可能无法完全匹配名义显著性水平,但如果样本量足够大且测试假设得到满足,它们应该接近。这种经验规模与名义规模之间的差异就是为什么进行这样的模拟研究,以了解名义规模与实际情况的匹配程度。

A/B 测试估计的统计性质

在本节中,你将学习 DGP 的属性以及协变量在 A/B 测试结果估计中的加入如何影响测试的表现。通过下面的代码,你将模拟有或没有协变量的 DGP 的 A/B 测试,并观察测试的表现如何随着是否在估计中包含协变量而变化。

import numpy as np
import random
from matplotlib import pyplot as plt
from tqdm import tqdm
import statsmodels.api as sm

def fn_variance(data: list, ddof: int = 0) -> float:
    """
    Calculate the variance of a given list of data.

    Parameters:
    data (list): The list of data to calculate the variance for.
    ddof (int): Delta Degrees of Freedom. The divisor used in calculations is N - ddof. Default is 0.

    Returns:
    float: The variance of the data.
    """
    n = len(data)
    mean = sum(data) / n
    return sum((x - mean) ** 2 for x in data) / (n - ddof)

def fn_generate_cov(dim: int, corr: float) -> np.ndarray:
    """
    Generate a covariance matrix of a given dimension with a specified correlation.

    Parameters:
    dim (int): The dimension of the covariance matrix.
    corr (float): The correlation value for the off-diagonal entries.

    Returns:
    np.ndarray: The generated covariance matrix.
    """
    acc = []
    for i in range(dim):
        row = np.ones((1, dim)) * corr
        row[0][i] = 1
        acc.append(row)
    return np.concatenate(acc, axis=0)

def fn_generate_multnorm(nobs: int, corr: float, nvar: int) -> np.ndarray:
    """
    Generate a multivariate normal distribution.

    Parameters:
    nobs (int): The number of observations in the distribution.
    corr (float): The correlation coefficient.
    nvar (int): The number of variables in the distribution.

    Returns:
    np.ndarray: The generated multivariate normal distribution.
    """
    mu = np.zeros(nvar)
    std = (np.abs(np.random.normal(loc = 1, scale = .5,size = (nvar,1))))**(1/2)
    # generate random normal distribution
    acc = []
    for i in range(nvar):
        acc.append(np.reshape(np.random.normal(mu[i],std[i],nobs),(nobs,-1)))

    normvars = np.concatenate(acc,axis=1)

    cov = fn_generate_cov(nvar,corr)
    C = np.linalg.cholesky(cov)

    Y = np.transpose(np.dot(C,np.transpose(normvars)))

    return Y

def fn_randomize_treatment(N: int, p: float = 0.5) -> np.ndarray:
    """
    Randomize the treatment assignment for a population.

    Parameters:
    N (int): The total population size.
    p (float): The proportion of the population to be treated. Defaults to 0.5.

    Returns:
    np.ndarray: A binary array where 1 indicates treatment and 0 indicates control.
    """
    treated = random.sample(range(N), round(N*p))
    return np.array([(1 if i in treated else 0) for i in range(N)]).reshape([N,1])

def split_columns(X: np.ndarray, a: float, b: float, p0: int) -> np.ndarray:
    """
    Splits the input array into two sections based on given percentages.

    Parameters:
    X (np.ndarray): The input array of size (n, p).
    a (float): The percentage of the first p0 columns to keep (between 0 and 1).
    b (float): The percentage of the remaining columns to keep (between 0 and 1).
    p0 (int): The index up to which to apply the first percentage.

    Returns:
    np.ndarray: The output array containing a% of the first p0 columns and b% of the remaining columns.
    """
    if not (0 <= a <= 1 and 0 <= b <= 1):
        raise ValueError("a and b must be between 0 and 1.")
    if not (0 <= p0 <= X.shape[1]):
        raise ValueError("p0 must be between 0 and number of columns in X.")

    first_part = X[:, :p0]
    second_part = X[:, p0:]

    first_indices = np.random.choice(first_part.shape[1], int(a * first_part.shape[1]), replace=False)
    second_indices = np.random.choice(second_part.shape[1], int(b * second_part.shape[1]), replace=False)

    return np.concatenate((first_part[:, first_indices], second_part[:, second_indices]), axis=1)

def fn_generate_data(tau: float, N: int, p: int, p0: int, corr: float, flagX: bool = False):
    """
    Generate synthetic data for experimentation.

    Parameters:
    tau (float): Treatment effect.
    N (int): Number of observations.
    p (int): Number of covariates.
    p0 (int): Number of covariates with nonzero coefficients.
    corr (float): Correlation for multivariate normal.
    flagX (bool): If True, return covariates. Defaults to False.

    Returns:
    tuple: Depending on flagX, either returns (Yab,T) or (Yab,T,X).
    """

    X = fn_generate_multnorm(N,corr,p)

    T = fn_randomize_treatment(N) # choose treated units
    err = np.random.normal(0,1,[N,1])
    beta0 = np.random.normal(5,5,[p,1])

    beta0[p0:p] = 0 #set the coefficient of all covariates after p0 to 0
    Yab = tau*T+X@beta0+err
    if flagX==False:
        return (Yab,T)
    else:
        return (Yab,T,X)

def fn_tauhat_means(Yt: np.ndarray, Yc: np.ndarray) -> tuple:
    """
    Calculate the treatment effect estimate and its standard error.

    Parameters:
    Yt (np.ndarray): Outcome for treated units.
    Yc (np.ndarray): Outcome for control units.

    Returns:
    tuple: The treatment effect estimate and its standard error.
    """
    nt = len(Yt)
    nc = len(Yc)
    tauhat = np.mean(Yt)-np.mean(Yc)
    se_tauhat = (np.var(Yt,ddof=1)/nt+np.var(Yc,ddof=1)/nc)**(1/2)
    return (tauhat,se_tauhat)

def fn_bias_rmse_size(theta0: float, thetahat: float, se_thetahat: float, cval: float = 1.96) -> tuple:
    """
    Calculate bias, RMSE, and test size for the parameter estimate.

    Parameters:
    theta0 (float): The true parameter value.
    thetahat (float): The estimated parameter value.
    se_thetahat (float): The standard error of the estimated parameter value.
    cval (float): The critical value for the hypothesis test. Defaults to 1.96.

    Returns:
    tuple: The bias, RMSE, and test size for the parameter estimate.
    """
    b = thetahat - theta0
    bias = np.mean(b)
    rmse = np.sqrt(np.mean(b**2))
    tval = b/se_thetahat
    size = np.mean(1*(np.abs(tval)>cval))
    # note size calculated at true parameter value
    return (bias,rmse,size)

def fn_run_experiments(tau: float, Nrange: list, p: int, p0: int, corr: float, flagX: bool = False, a: float = None, b: float = None) -> tuple:
    """
    Run experiments by generating synthetic data and estimate treatment effect.

    Parameters:
    tau (float): Treatment effect.
    Nrange (list): Range of number of observations.
    p (int): Total number of covariates.
    p0 (int): Number of covariates with nonzero coefficients.
    a (float, optional): Percentage of the first p0 columns to keep (between 0 and 1). Only used if flagX is True.
    b (float, optional): Percentage of the remaining columns to keep (between 0 and 1). Only used if flagX is True.
    corr (float): Correlation for multivariate normal.
    flagX (bool): If True, return covariates. Defaults to False.

    Returns:
    tuple: The treatment effect estimates and their standard errors, and 95% confidence interval.

    Note:
    In the flagX == 2 case, the function uses the split_columns function to select a% of the first p0 columns 
    and b% of the remaining columns from the X data, before performing the regression and estimating the treatment effect.
    """
    n_values = []
    tauhats = []
    sehats = []
    lb = []
    ub = []
    for N in tqdm(Nrange):
        n_values = n_values + [N]
        if flagX==False:
            Yexp,T = fn_generate_data(tau,N,p,p0,corr,flagX)
            Yt = Yexp[np.where(T==1)[0],:]
            Yc = Yexp[np.where(T==0)[0],:]
            tauhat,se_tauhat = fn_tauhat_means(Yt,Yc)            
        elif flagX==1:
            # use the correct covariates in regression
            Yexp,T,X = fn_generate_data(tau,N,p,p0,corr,flagX)
            covars = np.concatenate([T,X],axis = 1)
            mod = sm.OLS(Yexp,covars)
            res = mod.fit()
            tauhat = res.params[0]
            se_tauhat = res.HC1_se[0]
        elif flagX==2:
            # use fraction a of the correct covariates and fraction b of the remaining covariates
            assert a is not None and b is not None, "Please provide valid 'a' and 'b' when flagX is 2"

            Yexp,T,X = fn_generate_data(tau,N,p,p0,corr,flagX)

            Xreg = split_columns(X,a,b,p0)

            covars = np.concatenate([T,Xreg],axis = 1)
            mod = sm.OLS(Yexp,covars)
            res = mod.fit()
            tauhat = res.params[0]
            se_tauhat = res.HC1_se[0]

        tauhats = tauhats + [tauhat]
        sehats = sehats + [se_tauhat]    
        lb = lb + [tauhat-1.96*se_tauhat]
        ub = ub + [tauhat+1.96*se_tauhat]

    return (n_values,tauhats,sehats,lb,ub)

def fn_plot_with_ci(n_values: list, tauhats: list, tau: float, lb: list, ub: list, caption: str):
    """
    Plot the treatment effect estimates and their 95% confidence intervals.

    Parameters:
    n_values (list): List of number of observations.
    tauhats (list): List of treatment effect estimates.
    tau (float): True treatment effect.
    lb (list): List of lower bounds of the confidence intervals.
    ub (list): List of upper bounds of the confidence intervals.
    caption (str): Title for the plot.
    """
    fig = plt.figure(figsize = (10,6))
    plt.plot(n_values,tauhats,label = '$\hat{\\tau}$')
    plt.xlabel('N')
    plt.ylabel('$\hat{\\tau}$')
    plt.axhline(y=tau, color='r', linestyle='-',linewidth=1,
                label='True $\\tau$={}'.format(tau))
    plt.title('{}'.format(caption))
    plt.fill_between(n_values, lb, ub,
        alpha=0.5, edgecolor='#FF9848', facecolor='#FF9848',label = '95% CI')
    plt.legend()

无协变量的 DGP 实验

以下我们模拟数据,这些数据遵循一个 DGP,其中结果仅受处理和随机误差的影响。

y_i = tau*T_i+e_i

如果结果仅受到处理的影响,即使对于相对较小的样本量,处理效应参数的估计也是准确的,并且随着样本量的增加很快就会收敛到真实的参数值。在下面的代码中,参数 tau 的值设置为 2。

tau = 2
corr = .5
p = 10
p0 = 0 # number of covariates used in the DGP
Nrange = range(10,1000,2) # loop over N values
(nvalues,tauhats,sehats,lb,ub) = fn_run_experiments(tau,Nrange,p,p0,corr)

caption = """Estimates of the treatment effect parameter 
    for a randomized experiment without covariates"""
fn_plot_with_ci(nvalues,tauhats,tau,lb,ub,caption)

对于选定的样本量,验证这与进行包含截距的回归分析是一样的。

你可以通过对结果进行一个截距和处理的 OLS 回归来验证你获得了相同的结果。

N = 100
Yexp,T = fn_generate_data(tau,N,10,0,corr)
Yt = Yexp[np.where(T==1)[0],:]
Yc = Yexp[np.where(T==0)[0],:]
tauhat,se_tauhat = fn_tauhat_means(Yt,Yc)
# n_values = n_values + [N]
# tauhats = tauhats + [tauhat]
lb = lb + [tauhat-1.96*se_tauhat]
ub = ub + [tauhat+1.96*se_tauhat]

print(f"Parameter estimate and stadard error obtained by calculating the difference in means:{tauhat:.5f},{se_tauhat:.5f}")

const = np.ones([N,1])

model = sm.OLS(Yexp,np.concatenate([T,const],axis = 1))
res = model.fit()

print(f"Parameter estimate and stadard error obtained by running an OLS regression with an intercept:{res.params[0]:.5f},{ res.HC1_se[0]:.5f}")
Parameter estimate and stadard error obtained by calculating the difference in means:1.91756,0.21187
Parameter estimate and stadard error obtained by running an OLS regression with an intercept:1.91756,0.21187

运行 R 的蒙特卡洛迭代,计算偏差、均方根误差(RMSE)和规模

现在你将进行蒙特卡洛模拟,通过循环遍历 N 参数的值列表来增加样本量。你将计算每次迭代的测试 RMSE、偏差和经验规模。

这个 Python 脚本进行了一次实验模拟,研究样本量(N)在不考虑协变量的情况下如何影响 A/B 测试性能的偏差、RMSE 和规模。我们逐步解析如下:

1. estDict = {} 初始化一个空字典以存储实验结果。

2. R=2000 设置实验的重复次数为 2000。

3. for N in [10,50,100,500,1000] 遍历不同的样本大小。

4. 在这个循环中,tauhats=[], sehats=[] 被初始化为空列表,用于存储每次实验的估计处理效应 tauhat及其对应的标准误差 se_tauhat

5. for r in tqdm(range(R)): 遍历 R 次实验,tqdm 提供进度条。

6. Yexp,T = fn_generate_data(tau,N,10,0,corr) 为每次实验生成合成数据,具有预定义的处理效应 tau、观测数 N、10 个协变量、没有非零系数的协变量,以及预定义的相关性。

7. Yt=Yexp[np.where(T==1)[0],:]Yc=Yexp[np.where(T==0)[0],;]将合成数据分成处理组和对照组。

8. tauhat,se_tauhat=fn_tauhat_means(Yt,Yc) 计算处理效应估计值及其标准误差。

9. tauhats=tauhats+[tauhat]sehats=sehats+[se_tauhat] 将处理效应估计值及其标准误差附加到对应的列表中。

10. estDict[N]={‘tauhat':np.array(tauhats).reshape([len(tauahts),1]),’sehat':np.array(sehats).reshape([len(sehats),1])}将估计值存储在字典中,样本大小作为键。

11. tau0 = tau*np.ones([R,1]) 创建一个大小为 R 的数组,所有元素等于真实的处理效应。

12. 对于 estDict 中的每个样本大小,脚本计算并打印使用 fn_bias_rmse_size() 函数的偏差、RMSE 和处理效应估计的大小。

正如预期的那样,随着样本大小的增加,偏差和 RMSE 降低,大小接近真实大小 0.05。

estDict = {}
R = 2000
for N in [10,50,100,500,1000]:
    tauhats = []
    sehats = []
    for r in tqdm(range(R)):
        Yexp,T = fn_generate_data(tau,N,10,0,corr)
        Yt = Yexp[np.where(T==1)[0],:]
        Yc = Yexp[np.where(T==0)[0],:]
        tauhat,se_tauhat = fn_tauhat_means(Yt,Yc)
        tauhats = tauhats + [tauhat]
        sehats = sehats + [se_tauhat]
    estDict[N] = {
        'tauhat':np.array(tauhats).reshape([len(tauhats),1]),
        'sehat':np.array(sehats).reshape([len(sehats),1])
    }

tau0 = tau*np.ones([R,1])
for N, results in estDict.items():
    (bias,rmse,size) = fn_bias_rmse_size(tau0,results['tauhat'],
                                         results['sehat'])
    print(f'N={N}: bias={bias}, RMSE={rmse}, size={size}')
100%|██████████| 2000/2000 [00:00<00:00, 3182.81it/s]
100%|██████████| 2000/2000 [00:00<00:00, 2729.99it/s]
100%|██████████| 2000/2000 [00:00<00:00, 2238.62it/s]
100%|██████████| 2000/2000 [00:04<00:00, 479.67it/s]
100%|██████████| 2000/2000 [02:16<00:00, 14.67it/s]
N=10: bias=0.038139125088721144, RMSE=0.6593256331782233, size=0.084
N=50: bias=0.002694446014687934, RMSE=0.29664599979723183, size=0.0635
N=100: bias=-0.0006785229668018156, RMSE=0.20246779253127453, size=0.0615
N=500: bias=-0.0009696751953095926, RMSE=0.08985542730497854, size=0.062
N=1000: bias=-0.0011137216061364087, RMSE=0.06156258265280801, size=0.047

在 DGP 中进行的协变量实验

接下来,你将向 DGP 添加协变量。现在感兴趣的结果不仅依赖于处理,还依赖于一些其他变量 X。下面的代码模拟了包含 50 个协变量的 DGP。使用与之前无协变量模拟相同的样本大小和处理效应参数,可以看到这次估计值噪声更大,但仍然趋向于正确的解。

y_i = tau*T_i + beta*x_i + e_i

下图比较了两个 DGP 的估计值——你可以看到,当在 DGP 中引入协变量时,估计值的噪声增加了。

tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
Nrange = range(10,1000,2) # loop over N values
(nvalues_x,tauhats_x,sehats_x,lb_x,ub_x) = fn_run_experiments(tau,Nrange,p,p0,corr)

caption = """Estimates of the treatment effect parameter 
for a randomized experiment with X's in the DGP but no X's included in the estimator"""
fn_plot_with_ci(nvalues_x,tauhats_x,tau,lb_x,ub_x,caption)

# rerun experiment with no covariates
p0 = 0 # number of covariates used in the DGP
Nrange = range(10,1000,2) # loop over N values
(nvalues_x0,tauhats_x0,sehats_x0,lb_x0,ub_x0) = fn_run_experiments(tau,Nrange,p,p0,corr)

fig = plt.figure(figsize = (10,6))
plt.title("""Estimates of the treatment effect parameter 
for a randomized experiment with X's in the DGP but no X's included in the estimator, zoom in for large N""")
plt.plot(nvalues_x[400:],tauhats_x[400:],label = '$\hat{\\tau}^{(x)}$')
plt.plot(nvalues_x[400:],tauhats_x0[400:],label = '$\hat{\\tau}$',color = 'green')
plt.legend()

fig = plt.figure(figsize = (10,6))
plt.title("""
Treatment effect estimates from DGP with and without covariates, zoom in for large N
""")
plt.plot(nvalues_x[400:],tauhats_x[400:],label = '$\hat{\\tau}^{(x)}$')
plt.plot(nvalues_x[400:],tauhats_x0[400:],label = '$\hat{\\tau}$',color = 'green')
plt.legend()
100%|██████████| 495/495 [00:41<00:00, 12.06it/s]
100%|██████████| 495/495 [00:42<00:00, 11.70it/s]

重复实验时使用更大的样本大小是否能缓解问题?不一定。尽管样本大小增加,估计值仍然相当嘈杂。

tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
Nrange = range(1000,50000,10000) # loop over N values
(nvalues_x2,tauhats_x2,sehats_x2,lb_x2,ub_x2) = fn_run_experiments(tau,Nrange,p,p0,corr)

fn_plot_with_ci(nvalues_x2,tauhats_x2,tau,lb_x2,ub_x2,caption)

DGP with X — 在回归中添加协变量

在这一部分,你将使用与之前相同的 DGP:

y_i = tau*T_i + beta*x_i + e_i

现在,你将把这些协变量X包含在回归模型中。你会发现这显著提高了估计的精度。然而,请记住,这有点像是“作弊”——在这种情况下,你从一开始就包含了正确的协变量。

在现实世界的情境中,你可能不知道哪些协变量是“正确”的,可能需要尝试不同的模型和协变量。

tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
Nrange = range(100,1000,2) # loop over N values
# we need to start with more observations than p
flagX = 1
(nvalues2,tauhats2,sehats2,lb2,ub2) = fn_run_experiments(tau,Nrange,p,p0,corr,flagX)

caption = """Estimates of the treatment effect parameter 
for a randomized experiment with X's in the DGP, 
estimates obtained using regression with the right Xs"""
fn_plot_with_ci(nvalues2,tauhats2,tau,lb2,ub2,caption)

如果你使用一些会影响结果的 X 和一些不会影响结果的 X,会发生什么?

本节探讨了在回归模型中包含一些相关的和一些不相关的协变量。这模拟了一个现实世界的情境,在这种情境下,可能不清楚哪些协变量会影响结果。

尽管包含了一些无影响的变量,但可以观察到,与没有包含任何协变量的情况相比,整体估计趋向于改善。然而,包含不相关的变量可能会引入一些噪声和不确定性,使得估计可能不如仅包含相关协变量时那样精确。

总之,理解数据中协变量的影响对于提高 A/B 测试结果的精度和可靠性至关重要。本教程探讨了像 RMSE、偏差和规模这样的估计量的统计特性,并演示了如何通过蒙特卡洛模拟来估计和理解它们。它还强调了在 DGP 和回归模型中包含协变量的影响,突出了在实践中仔细选择模型和假设检验的重要性。

# Use same DGP as before
tau = 2
corr = .5
p = 100
p0 = 50 # number of covariates used in the DGP
a = 0.9
b = 0.1
Nrange = range(100,1000,2) # loop over N values
# we need to start with more observations than p
flagX = 2
(nvalues3,tauhats3,sehats3,lb3,ub3) = fn_run_experiments(tau,Nrange,p,p0,corr,flagX,a,b)

caption = f"""Estimates of the treatment effect parameter 
for a randomized experiment with X's in the DGP, 
estimates obtained using regression with the {100*a:.1f}% of the correct Xs and {100*b:.1f}% of the irrelevant Xs"""
fn_plot_with_ci(nvalues3,tauhats3,tau,lb3,ub3,caption)

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

一种更好的符号回归方法,通过明确考虑单位

原文:towardsdatascience.com/a-better-symbolic-regression-method-by-explicitly-considering-units-35b3630165b

通过提供根植于基本概念的更具可解释性的模型,有助于将机器学习与经典科学和工程方法相结合

LucianoSphere (Luciano Abriata, PhD)Towards Data Science LucianoSphere (Luciano Abriata, PhD)

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

--

图由作者通过结合不同的 Dall-E-2 生成图像创建。

符号回归是一种技术,帮助我们理解不同数据之间的关系,通过找到描述这些关系的数学方程。我对符号回归方法充满期待,并且大力支持它们,因为通过提供明确的方程,它们原则上是高度可解释的,以直接的方式 - 这与大多数现代 AI 模型形成对比,后者像黑箱一样运作,我们无法理解其工作原理和原因,从而难以知道它们如何以及为什么有效。

由 Tenachi 等人提出的新工作,现已在 arXiv 上以预印本形式发布,提出了一种新方法,该方法利用深度强化学习来寻找数据集变量的方程,同时考虑数据相关的单位。这种方法有助于消除物理上不可能的解,并通过限制方程生成器的自由度来提高性能。

索引

- 介绍 - 关于符号回归及介绍新方法 - 新符号回归方法的工作原理 - 总结说明、预印本和代码

介绍

自然科学和工程学中的一个基本问题是发现给定系统的自变量和因变量之间的定量关系。虽然可以通过类似黑箱模型的现代常规人工智能方法完美地建模这些关系,但更理想的方式是通过符号方程量化这些关系,比如说 a=F/mI=I0exp(-kt)

为什么?好吧,有很多原因:

  • 方程是可解释的,因此对人类思维来说更易理解,可能甚至通过已经知道的科学或工程概念来连接变量之间的关系。例如,在 a=F/m 中,我们可以直接理解施加更强的力会使物体加速更快,以线性方式表现;而在 I=Ioexp(-kt) 中,我们知道因变量的衰减是随着时间的指数变化,从中我们可以利用数学得到半衰期时间的方程、线性化形式(通过对数)等。

  • 如果可以用简单的术语解释,连接变量的方程有很大可能直接与被建模的科学或工程问题的基础概念、想法和公理相关。例如,考虑应用于放射性衰变的 I=I0exp(-kt),从其导数 (dI/dt = -kt) 我们可以理解同位素衰变的速率与样本中剩余的放射性核数成正比,因此这是一个一级过程。

  • 通过方程传播自变量(输入)以建模结果变量(因变量)是一种极其快速的计算,几乎是即时的,相比之下,将输入传播通过神经网络的所有单元则更为复杂。这对个别预测可能没有影响,但在需要进行大量预测时可能很重要。此外,通过符号方式关联变量的方程可以通过简单地插入拟合方程来无缝集成到其他程序中。例如,查看一个很好的用例 这里。

  • 重要的是,分析地关联变量的方程可能在输入数据采样的领域之外更安全地进行外推。

对于常规的机器学习模型,这些点通常不适用,至少不是以如此直接的方式,因为这些模型通过大量嵌套函数和组合来混合和卷积信号,直到它们能够正确建模因变量,虽然这些模型可能表现得非常好,但解释性往往很差——如果有的话。

上述解释说明了当一个关系可以通过方程建模时,你最好选择这种方法。但当你找不到数学表达式来关联变量时,会发生什么呢?这时你可以尝试常规神经网络,或者尝试符号回归。

关于符号回归,并介绍新方法

符号回归的目标是找到适合给定数据集变量的自由形式符号解析函数,这比仅仅在线性或非线性函数中拟合系数的方法要更为通用。简单来说,符号回归不仅是拟合方程,更是找到它们的表达式(然后是拟合其中的系数)。从介绍中总结但用不同术语表述,符号回归的优势包括紧凑性、泛化能力(这意味着如果正确,解析表达式在训练范围外的外推能力会更强)以及可理解性和可解释性。

为了明确这些优势,并理解符号回归在建模数据集中的作用,请查看我从近期文献中涉及的符号回归在科学应用中的最新进展和示例:

## 符号回归的实际应用

比起常规神经网络,这种方法较少像黑箱,提供的模型不仅预测数据,还能进行合理化…

## 谷歌提出新的方法推导量子力学术语的解析表达式…

该新方法以符号形式推导准确的泛函(量子力学计算的元素),因此…

谷歌提出新的方法推导量子力学术语的解析表达式

不幸的是,实现符号回归以发现新的物理定律是极具挑战性的。更难的是获得紧凑且简单的方程,这些方程可以真正用基础科学或工程概念进行解释。我强调这一点是因为许多符号回归模型提出的方程非常复杂,以至于很难解释,并且与标准机器学习模型相比几乎没有或根本没有贡献。而在最成功的案例中,找到足够简单的方程通常需要很长的执行时间,因为程序需要探索可能的数学操作的巨大树的不同分支,这些操作必须组合并测试以构建可能的方程。

最后这两点,即对简化方程的需求和可能快速收敛到这些方程,是 Tenachi 等人新工作的主要动机。以下是他们的主要贡献:作者意识到,通过符号回归程序连接的变量的单位对方程的形状施加了强有力的约束。他们探索了如何利用这一事实来优化方程搜索,从而提出了一种将物理单位信息纳入符号回归程序的具体框架。

作者意识到,通过符号回归程序连接的变量的单位对他们寻找的方程的形状施加了强有力的约束。他们利用这一点来优化方程搜索,并提出了一种将物理单位信息纳入符号回归程序的框架。

通过在方程搜索过程中将单位作为约束,新框架有效地解决了试验表达式的巨大搜索空间的组合挑战。搜索空间显著缩小,结果是方程搜索速度更快。此外,作者发现该程序生成的表达式更简单,因此比其他符号回归方法获得的表达式更易解释且更准确。

新的符号回归方法如何工作

其核心是框架包括一种新颖的符号嵌入,这里针对物理学进行了调整,但原则上可以更广泛地扩展,这使得能够控制在部分组合的数学表达式中生成的每个符号的单位。这使得程序能够自动将搜索空间引导到单位保持一致的路径上。当程序运行时,它使用递归神经网络生成解析表达式,并在单位约束下通过强化学习的步骤进行循环,从而得到物理上有意义的输入变量组合。

更详细地说,该过程首先通过使用二叉树生成符号表达式,其中每个节点表示库中可用符号的一个符号。表达式被视为类别向量的序列,并使用递归神经网络生成标记序列。(顺便说一下,这些类别向量可以被人为调整以纳入先验知识;在这一阶段采用了一些先验,例如限制分析表达式的最大可能大小,允许不超过两级的嵌套三角函数操作,并且不允许指数和对数运算符的自我嵌套,这在科学中是不寻常的,等等。)

生成的表达式受到物理单位约束,这些约束通过一种程序进行计算,该程序在可能的情况下计算所需的单位,否则将其留作自由。然后,在强化学习部分,生成一组试验符号函数,并通过与数据对比计算每个函数的奖励。网络随后需要生成一批新的试验函数,通过强化与高奖励值相关的行为来鼓励生成更好的结果。这种方法强化基于不仅是强化网络的输出,还基于从先验分布得出的局部单位约束的候选项,这确保了标记选择的物理正确性。

值得注意的是,该方法允许候选函数包含具有固定单位的常数,但数值自由,从而可以模拟问题中存在一些未知物理尺度的情况。最后,常数的最佳值通过使用标准优化程序的梯度下降找到。

结论、预印本和代码

在展示了新方法在一系列天体物理学示例中的有效性后,这项新工作的作者旨在为其他物理科学构建一个强大的通用符号回归算法。

你可以在 arXiv 上阅读完整的预印本:

[## 深度符号回归与单位约束:朝着自动化发现…

符号回归是研究自动化搜索与数据拟合的解析表达式的算法。虽然…

arxiv.org](https://arxiv.org/abs/2303.03192?source=post_page-----35b3630165b--------------------------------)

你可以在这里尝试该程序:

[## GitHub - WassimTenachi/PhySO: 物理符号优化

物理符号回归(\(\Phi\)-SO)包 physo 是一个完全利用…

github.com

你还可能会觉得预印本的首席作者的演讲很有用,以及他在推特上发布的讨论:

正如 Tenachi 在他的推特讨论中总结的那样,

虽然神经网络是建模物理系统的出色工具,但它们缺乏可解释性和泛化能力。[新方法] 提供了打开这些黑箱并恢复基本方程的机会,作为物理学家,我们都知道并喜爱这些方程。

www.lucianoabriata.com 我写作和拍摄的内容涵盖了我广泛兴趣领域中的一切:自然、科学、技术、编程等。 成为 Medium 会员 以访问所有故事(平台的关联链接,为我带来少量收入,但对你没有费用),并且 订阅获取我的新故事 通过电子邮件。要 咨询关于小工作, 请查看我的 服务页面在这里。你也可以 在这里联系我

更好的分析功能发布影响的方法

原文:towardsdatascience.com/a-better-way-to-analyze-feature-release-impact-72529f907ccd?source=collection_archive---------14-----------------------#2023-05-23

或者——为什么简单的“前后”比较可能导致糟糕的产品决策

Iyar LinTowards Data Science Iyar Lin

·

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

--

作者提供的照片 — 使用 DALL-E 2

A/B 测试是估计产品分析中因果效应的金标准。但在许多情况下,它们并不可行。最常见的情况之一是功能发布。

在这篇文章中,我将讨论使用简单的“前后”比较来衡量功能发布影响的常见做法,以及这些分析中常见的偏差。我还会提供一些建议,说明如何减轻这些偏差。

一些背景信息

很多时候,公司在发布新产品功能或应用版本时,未进行 A/B 测试来评估其对主要 KPI 的影响。这可能由于各种原因,如流量较低或技术复杂性较高。

在特定日期将功能部署给所有用户后,产品经理通常会通过简单的“前后”分析来评估功能发布的影响:比较发布后短时间内的 KPI 与发布前相同时间段的 KPI。

尽管直观,但这种幼稚的比较可能忽略重要的偏差来源。

以下我将讨论简单的前后分析中最常见的两个偏差来源及其如何导致错误结论。

偏差 1:时间效应

一个常见的情况是产品经理进行“前后”分析并获得积极的结果。

然而,查看 KPI 随时间变化的图表时,他们可能会遇到令人失望的结论:

作者提供的照片

无论发布如何,KPI 在整个时期内都呈上升趋势,而发布本身似乎有负面影响。简单的“前后”比较假设没有时间动态,这可能是非常错误的,就像上面所示的情况一样。

偏差 2:业务组合的变化

尽管时间效应引入的偏差可能非常明显,但其他的可能更为微妙。

在另一种情况下,产品经理可能会测得负面的“前后”发布影响。绘制 KPI 随时间变化的图表似乎没有提供替代结论:

作者提供的照片

许多公司会在这里停下,假设发布效果不好,需要回滚。

然而,在许多情况下,发布前后期间的差异可能是由于用户组合的变化。这可能是偶然发生的,但通常与伴随功能发布的营销活动有关。

为了使例子具体化,可能是在发布后的某一时期,Android 用户的比例显著上升,相比于发布前的时期。

作者提供的照片

在这个具体的例子中,那些 Android 用户的转化率往往低于 iOS 用户,但这些组内的发布效果实际上是积极的:

作者提供的照片

因此,考虑到设备因素,发布的影响实际上是积极的。总体差异与组内差异相反的情况是辛普森悖论的经典例子。

这是否意味着我们不能没有 A/B 测试?

上述情况相对简单。时间效应可能包括复杂的趋势和每日季节性变化,细分比例变化可能更微妙,并且分布在许多子集中。

可能会给人一种分析功能发布数据毫无意义的印象。然而,我认为情况不一定是这样。

引入发布影响算法

Loops工作时,我设计了一个算法来自动和透明地处理上述偏差。由于业务和知识产权原因,我无法分享完整的实现细节,但以下是一个总体概述:

  1. 使用 ML 算法找到在发布前后期间中比例变化最大的细分市场。

  2. 在每个细分市场中,分别建模时间趋势和季节性以及发布影响。

  3. 对所有细分市场内估计的发布影响进行加权平均,以得出最终的影响估计。

测试算法的有效性

你无法确定任何方法是否适用于特定的数据集。然而,你可以通过使用过去的 A/B 测试来获得粗略估计。

例如,执行了一个包含控制组和处理组的 A/B 测试。比较这两个组之间的平均 KPI 可以得到对处理影响的无偏估计。这作为我们的“金标准”。

我们将测试前的用户段命名为“前控制”。将前控制人群与处理人群进行比较类似于我们在前后分析中所做的比较。

使用多种不同的测试,我们可以将“金标准”估计与“前后”估计进行比较,看看它们的接近程度。

Loops工作时,我可以访问来自数十个客户使用我们系统的数百个 A/B 测试。使用上述基准方法,我们发现该算法的准确性远远优于简单的“前后”比较。

总结

我希望到此时读者已经意识到使用简单的“前后”比较方法的风险,并且上述算法将为任何希望更好地评估功能发布影响的人提供基础。

这篇文章的修改版已于 https://getloops.ai 在 2023 年 5 月 11 日发布。

一种更好的在没有数据的情况下获得结果的方法

原文:towardsdatascience.com/a-better-way-to-get-results-without-data-3d75cd93c424

想象一下有一个数据集,并希望看到零而不是空单元格。这听起来简单?实际上,这个案例有一些陷阱。让我们来深入研究一下。

Salvatore CagliariTowards Data Science Salvatore Cagliari

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

--

图片由Ramón Salinero拍摄,来源于Unsplash

介绍

几周前,我已经写过一篇关于这个主题的文章:

## 如何在 Power BI 中在没有数据的情况下显示结果

这个标题似乎不直观。为什么在没有数据的情况下我还要要求结果?让我们看看请求…

towardsdatascience.com

在那篇文章中,我使用 Power Query 生成了额外的数据行来填充数据表并获得所需的结果。

现在我找到了一种更简单的方法来解决这个挑战。

首先,我将解释这个挑战。如果你知道我上面链接的文章,以下部分描述了我如何找到新的方法。然后我将向你展示新的解决方案。

挑战

想象一个报告,其中包含显示多个任务及其相关成本和每个任务状态的表格:

图 1 — 起始点的表格(图由作者提供)

现在,想象一个类似的表格。但现在在“失败”状态中没有任务:

图 2 — 缺失状态的表格(图由作者提供)

现在,我的客户希望看到零而不是空单元格,并且他希望看到所有可能的状态,即使没有任务具有特定状态,如上图所示。

问题是,如果数据表中没有值,则不会执行任何计算,这意味着不可能显示零而不是空单元格。

新方法的灵感

几天前,我的一位客户问我关于他的 Power BI 报告的问题。

请理解,我只能在这里展示一些细节。

总而言之:他计算了一个百分比,并从 1 中扣除了结果金额。

这个度量看起来是这样的:

Measure Name = 1 - ( CALCULATE(SUM(column1)
                               ,[Column2] = "Filter Text"
                               )
                           / 12345 )

结果是,他总是得到 100 %,而期望得到一个空结果。

经过一些测试,我意识到“1 –”部分是罪魁祸首。

当 CALCULATE() 函数返回一个值时,结果会被正确计算。

如果没有 CALCULATE() 的结果,引擎计算 1–0 = 1,即 100 %

Power BI 将值 1 视为常量,并且没有应用任何过滤器。

结果是,当表中没有数据时,Power BI 返回了 100 %。

我必须添加一个 IF() 来解决这个问题,检查 CALCULATE() 函数是否返回了一个值:

Measure Name = 
 VAR Result = CALCULATE(SUM(column1)
                        ,[Column2] = "Text"
                        )
RETURN
IF (NOT ISBLANK(Result)
     , 1 - ( Result / 12345 )
     )

现在,只有当 CALCULATE() 返回一个值时,度量才返回一个值。

反转想法

从中产生了一个想法:将这个“问题”反转并转化为解决方案。

如果我能向我的解决方案中添加一个度量,并计算 0 + SUM(‘table’[column]) 呢?

在这种情况下,如果 SUM 没有返回任何结果,我将得到 0。而当 SUM() 返回一个值时,添加 0 不会改变结果。

但等等,这里有个问题:当没有状态为“失败”的行时,我怎么能显示任何结果?

为了解决这个问题,我必须稍微改变我的数据模型。

最初,我的数据模型只包含包含值的表。

为了强制计算,我需要一个包含所有状态的表,并将其用作维度,以便为表中的每一行强制设置过滤上下文:

图 3 — 状态表(作者绘制)

然后,我必须向我的演示数据表中添加一个关系:

图 4 — 包含两个表的数据模型(作者绘制)

现在我更改了我的报告,从状态表中获取状态列,并添加了一个新的度量:

Costs = 0 + SUM('Demo Data 2'[Cost])

下面,你可以看到原始表的数据(包括所有状态的数据),以及使用新解决方案的没有状态失败的表的数据:

图 5 — 比较两个结果(作者绘制)

这是我客户需要的结果。

结论

数据集是虚构的,像之前的文章一样,在 Excel 中从头开始创建。

虽然我的第一个解决方案是建立在数据表是我所拥有的一切的假设上,但现在我必须创建一个新的状态表,并确保这个表中包含所有可能的状态。

你可能可以从数据源中提取这些信息。

如果没有,你可以使用我基于 Power Query 的旧解决方案,或者使用 Power BI 的“输入数据”功能创建一个表。

无论你如何添加额外的表格,你都需要生成一个过滤上下文,以便度量值可以始终返回一个值。

照片由 Bill Jelen 提供,来源于 Unsplash

如果你对 Power Query 不熟悉,可以阅读我之前的文章以了解更多关于这个强大工具的信息。

在某个时刻,这可能会节省你一天的时间。

[## 通过我的推荐链接加入 Medium - Salvatore Cagliari

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

medium.com](https://medium.com/@salvatorecagliari/membership?source=post_page-----3d75cd93c424--------------------------------)

线性代数的全景视角:方程组、线性回归和神经网络

原文:towardsdatascience.com/a-birds-eye-view-of-linear-algebra-systems-of-equations-linear-regression-and-neural-networks-fe5b88a57f66

谦逊的矩阵乘法及其逆几乎是许多简单机器学习模型中的核心内容

Rohit PandeyTowards Data Science Rohit Pandey

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

--

图片由 midjourney 提供

这是进行中的线性代数书籍《线性代数的全景视角》的第四章。到目前为止的目录:

  1. 第一章:基础

  2. 第二章:映射的度量——行列式

  3. 第三章:为什么矩阵乘法是这样?

  4. 第四章(当前):方程组、线性回归和神经网络

  5. 第五章:秩和零化以及为什么行秩等于列秩

本博客中的所有图片,除非另有说明,均由作者提供。

I) 引言

现代人工智能模型利用高维向量空间来编码信息。并且我们 用于推理高维空间及其映射的工具是线性代数。

在这一领域内,矩阵乘法(以及它的逆)几乎是构建许多简单机器学习模型所需的全部内容。这也是为什么花时间深入理解它是一个很好的投资。这就是我们在第三章中所做的。

这些简单模型,虽然在自身之内有用,却构成了更复杂的机器学习和人工智能模型的基础,这些复杂模型具备最先进的性能。

在这一章中,我们将介绍一些这些应用(从线性回归到初级神经网络)。

但首先,我们需要转到最简单的情况和最简单的模型——当数据点的数量等于模型参数的数量时,即求解线性方程组的情况。

II) 线性方程组

我们终于来到了线性代数的核心(在本书的背景下)。求解线性方程组是我们最初发现线性代数的方式,这个领域的大多数概念的动机在这个应用中有着深远的根基。

让我们从简单的一维情况开始。除法的概念根植于一维线性方程中。

ax = b

这个方程的意思是,“哪个数字在乘以a后得到b”。解决方案定义了标量除法:

x = b/a _(1)

这是在一维的情况下,即x。当进入多维时,最有趣的事情发生了。所以我们不仅有一个变量x,还有n个变量(x_1, x_2, x_3, …, x_n)。

一旦进入多维空间,线性代数就会出现。

当问题变得多维时,线性代数就会出现。图像来自 midjourney。

现在,我们的方程应包括n个变量,即x_1, x_2, x_3, …, x_n。就像x之前有系数a一样,这次每个x_i都有自己的系数a_i

但与一维情况不同,这个方程不足以求解x,因为它不能唯一地确定x。例如,我们可以随意选择x_2, x_3,…, x_n(例如:全部为 0),只有这样我们才能得到唯一的x_1值。如果我们已经知道了x_1, x_2, …, x_n的值,并且想以方程组的形式传达它们,它们会看起来像这样:

因此,我们需要n个方程(等于变量的数量)。现在可以通过“混合”上述方程来创建任何一般系统,即取它们的线性组合。这允许添加任意两个方程,并将任意方程乘以任意标量。这些操作显然不会改变系统的解(或其存在性问题)。

例如,我们可以将第二个方程乘以三加到第一个方程中,得到:

然后我们可以用这个方程替换第一个方程(x_1=4.6),得到的方程组仍然有相同的解(因为我们可以通过从新的第一个方程中减去三倍的第二个方程来撤销我们所做的更改)。如果我们以这种方式非常彻底地“混合”,随机替换其中一个方程,并重复多次,我们将得到n个方程,每个方程涉及所有n个变量的倍数。

系数 a_{ij} 看起来非常像一个方阵的元素。确实,根据我们在第三章中学到的矩阵向量乘法(见 III-A 节中的动画-1),我们可以将方程组表示为:

方程(1)线性方程组的矩阵形式。

就像在方程(1)中我们取了乘法逆来得到标量变量 x (x=b/a) 一样,这里我们取矩阵乘法的逆来得到向量 x

我们已经达到了一切开始的标志性方程。线性代数的起点。线性方程组。计算逆矩阵及其相关操作有一个完整的科学背景,我们将在后续章节中讨论。

注意,A 矩阵的行数是系统中的方程数,列数是变量数。

从几何上讲,每个方程在空间中都是一个维度降低的超平面。考虑以下方程组:

这个方程组的解是(x=0, y=0, z=1)。由于这个方程组中有三个变量(x, yz),因此矢量空间是三维的,如下图-1 所示。请参阅该图以获取后续讨论。

x=0 方程对应于这个三维空间中的黄色超平面。由于该方程表示增加一个约束,满足该方程的空间的维度降低一个(3–1=2)。类似地,y=0 方程对应于蓝色超平面,也二维。现在我们有两个方程,因此两个约束。两个方程都满足的子空间的维度将降低两个,变成3–2=1维。一维子空间就是一条直线,实际上我们得到的是绿色直线图-1。最后,当我们加入第三个方程 x+y+z=1,它由粉色平面表示。现在我们有三个方程/约束。它们将三维空间的维度限制为 3。因此,我们剩下的维度是3–3=0(即一个点),确实,我们得到红色点,它是同时满足系统中所有三个方程的唯一矢量空间元素。

图-1:三变量的方程组。空间是三维的。每个方程切割出一个低一维的超平面。两个方程的组合去除两个维度,依此类推。图像由作者提供。

在上面的例子中,我们有 n 个方程和 n 个变量。一般来说,它们不一定相同。假设有 n 个方程和 m 个变量。从上面的图可以清楚地看出:

  • n<m时,我们有更多的变量而不是方程,系统具有无限解。可以把方程看作是要求。要求指定得不够多,可能的解就会很多。

  • n>m时,我们将有更多的方程而不是变量。现在有了太多的约束条件,无法在向量空间中找到满足所有约束的点。

  • 所以当n=m时,我们应该总是有一个唯一解?不完全是,还有一些情况可能会出错(所有三种情况)。

II-A) 问题发生的原因

一致性

使任何系统没有解的一个因素是矛盾。例如,第一个方程是2x+y=3,第二个方程是 2x+y = 5。现在,无论我们再添加多少个方程,都无法同时满足这两个方程(它们相互矛盾)。任何包含这两个方程的系统都是不一致的。

依赖性

接下来,系统可能会欺骗我们,看起来好像有比实际更多的方程式。最明显的情况是如果我们简单地复制其中一个方程。这将使系统增加一个方程,使得技术上我们现在有了m+1个方程。然而,很明显,我们并没有真正增加任何新信息。我们新增的方程是多余的。这被称为依赖系统,因为我们的一些方程没有带来任何新的信息,而是“依赖”于其他方程。如果我们直接抄袭一个方程,那将很容易被发现。但可以以更隐蔽的方式做到这一点。我们可以通过对几个方程进行线性组合来创建一个新的方程。线性组合可以做到几乎不被发现混入了一个“冒牌方程”。当然,一旦这样的依赖方程被加入到系统中,就很难分辨哪个是“冒牌”方程,就像下面的三只蜘蛛侠一样。

两个鲍勃。但只能有一个。另一个是冒牌的。很难判断哪一个。就像通过对其他方程进行线性组合引入的依赖方程一样。图像来源:midjourney。

一致性和依赖性这两个问题在A矩阵(来自方程(1))方面表现相同。向量b然后决定系统是否不一致(没有解)或依赖(无限多个解)。发生的情况是,A的某些行变成了其他行的线性组合。

II-B) 数据分析的情况

对我们最感兴趣的情况是建立在数据之上的模型。如果你有独立收集的数据点,并且数据收集过程是随机的(就像数据收集应该是的那样),几乎可以肯定你的数据矩阵的行不会线性相关(在概率术语中,"几乎肯定")。所以,我们不必担心得到一个不一致或相关的系统。

此外,我们通常有比列数(变量/特征)多得多的行(数据点)。所以,矩阵将是“瘦长的”,其中 n>m

一个高而瘦的矩形数据矩阵,行数多于列数。图片由 midjourney 提供。

我们正处于无解方程组的领域。如果我们选择任何 mn 个方程并删除/忽略其余的方程,那么我们将回到 m=m 的情况,这时方程数和变量数相等,现在将会有一个唯一的解。

因此,超平面对应于系统中方程的总数是 (n 选择 m) 个点。这些点中没有一个点会同时位于所有超平面上。

尽管没有一个点同时满足所有方程,我们仍然可以问:“在整个向量空间中哪个点最接近满足所有方程”。这就是线性回归的作用所在。

III) 线性回归

线性回归绘制一个最接近你数据点的线性超平面。就像忍者尝试以一种尽可能接近最多叶子的方式挥动他的剑一样。图片由 midjourney 提供。

首先,在线性回归的背景下,矩阵和向量的命名与方程组有所不同。现在,系数矩阵 A 变成了包含数据的矩阵,我们称之为 X。这个矩阵的每一行,即行向量 x_i,是一个数据条目。向量 x 包含了方程 (1) 中的未知变量。现在,未知变量是线性回归系数,我们用 𝛽 表示。最后,线性系统的右边向量 b 变成了包含因变量的向量,我们称之为 y。因此,在线性回归的背景下,方程 (1) 变成了:

Eq (2):线性回归的基本方程。图片由作者提供。

展开后,这个方程看起来如下:

注意第一列的1。这对应于常数项。例如,在单变量的情况下,如果没有那一列,我们的模型将会是:y = m.x. 这将排除像 y=x+1 这样的直线。如果要考虑像 y = mx+c (我们在大多数情况下确实需要)的直线,我们需要那一列1

就像线性方程组一样,我们希望对两边进行X的乘法逆运算,找到𝛽。如图所示:

这个方程没有任何意义。矩阵 X 是矩形的,所以它不能被逆转。

不幸的是,这没有意义。只有方阵才可以逆转。矩形矩阵则不行。我们的数据矩阵 X 是一个矩形矩阵,行数(n)大于列数(m)。

一种理解这一点的方法是,它表示一个方程数量多于变量数量的线性系统,因此第二部分的论点适用。就矩阵背后的线性映射而言,对于一个“瘦长”的矩阵(如 X 所示),行数多于列数,它不会是一个一对一的映射(同一空间中的多个点会映射到第二个空间中的同一点,这种情况是不允许的)。

是否有一种“技巧”可以让乘以 𝛽 的矩阵变成方阵?矩阵 X 目前是 n⨉m。如果我们乘以另一个 m⨉n 的矩阵 U,那么得到的矩阵 V 将是方阵 m⨉m

通过对方程(2)进行 U 的左乘,我们可以逆转结果矩阵并得到 𝛽。

现在,问题是,我们从哪里得到这个 U 矩阵(m⨉n)?我们拥有的是 X 矩阵,n⨉m(即数据本身)。我们可以做的一件事是将 X 矩阵翻转,使其行变成列,列变成行。这种对矩阵的操作称为转置,记作 X^T,如下图所示。

矩阵的转置。图片来源:维基百科 关于转置的文章。

因此,我们可以用 X 的转置来替代 U。这将得到:

方程(3):线性回归系数。

我们现在已经找到了回归模型的系数。如果我们得到一个新的数据点,x_new(以行向量的形式),我们可以将其与𝛽进行点积,从而获得相应的y

其中 𝛽 由上述方程(3)给出。现在,我们提供了用X^T替代U的动机,因为这是一个显而易见的选择,符合我们所需的维度。然而,相同的公式还有更强的数学动机。

III-A) 数学动机

我们需要为𝛽(方程(3)中的那个)提供一个具体的值。因此,让我们思考不同𝛽值的情况。如动画-1、第三章 III-A 节(矩阵乘法)中所述,我们可以将 X𝛽 理解为将 X 的列向量拆开并通过线性组合将它们组合成一个单一的列向量。更具体地说,我们将 X 的第一个列向量乘以𝛽向量的第一个元素,将第二个列向量乘以第二个元素,以此类推,然后将所有结果相加。因此,当我们改变𝛽向量时,我们就在探索矩阵 X 的“列空间”,即通过 X 的列向量的线性组合可以得到的所有向量的集合。然后,我们还有向量 y,这是我们方程(2)的右侧。就像 X 的列向量一样,这个向量的维度也是 n(数据点的数量/矩阵中的行数)。

这个向量空间(包含 yX 的列向量)的维度是 n。另一方面,X 的列向量的数量是 m。请记住,我们的情况是 n>>m。所以,X 的列空间(由这些 m 个向量张成的空间)的维度是 m。这比 n 要低得多,而 n 是那些列向量所在的更大空间。

向量 y 生活在与 n 维度相同的更大空间中。由于列向量的数量 mn 小得多,因此向量 y 几乎肯定不会在由 X 的列向量所张成的列空间中。

如果是这样,我们会有一个𝛽可以完全满足所有方程。这是不可能做到的(如前所述)。但是,我们仍然希望找到一个𝛽,使其尽可能接近向量 y。为此,我们需要最小化 yX𝛽之间的距离。

让我们来看一个具体的例子。假设我们有一个数据集,其中 n=3 个数据点。在我们的回归模型中,我们选择 m=2 个特征。方程 X𝛽=y 看起来是这样的:

例如,矩阵 X 有两个列向量,[1,1,1][2,5,7]。它们存在于一个三维空间中(n=3)。这两个列向量在下面的图 2 中用蓝色绘制。由这些向量(列空间)张成的空间是二维的(m=2),其中一部分被粉色阴影覆盖。

图 2:展示线性回归作为数据矩阵 X 的列空间探索。图片由作者提供。

现在,看看黑色向量、灰色向量和红色向量形成的三角形。黑色向量是 X𝛽,红色向量是 y,灰色向量是 d。这三者形成一个三角形,因此满足:

我们希望找到这个 d 向量在 X 的列空间(由 𝛽 控制)中最接近 y 的点。

我们通过将列向量的转置与其自身进行矩阵乘法来获得列向量的平方长度。

最后,让我们对 𝛽 取导数并设置为 0。这将给出最小化向量 d 的平方长度的 𝛽。

在这里,我们使用来自矩阵宝典的方程(78),[2]。这导致:

这与方程(3)是相同的。

我们在这里用 X 的列空间来激励(如[1]中更详细地解释)。相同的方程也可以被激励为最小化预测和实际值向量 y 中的平方误差项。这种方法在[2]中涵盖。

III-B) 在线线性回归

在线线性回归。数据以恒定的流入方式每天进入。我们需要在这个过程中保持我们的线性回归模型的最新状态。图像来源于 midjourney。

到目前为止,我们把线性回归看作一个静态模型,其中数据矩阵 X 和对应的响应向量 y 已经给定。很常见的是,数据会随着时间作为一个恒定的流入。一些数据点今天可能会丢失,明天可能会更多,以此类推。我们希望我们的模型使用一个滚动窗口的数据(比如 30 天),并且参数每天更新一次,针对过去 30 天的数据。显而易见的方法是每天使用过去 30 天的数据应用方程(3),并每天刷新参数。但如果数据点的数量 n(矩阵 X 的行数)非常大呢?这会使方程(3)的计算变得非常昂贵。如果你考虑昨天的模型与今天的模型。由于滚动窗口,大部分数据都是相同的。第 31 天,我们使用了第 1 天到第 30 天的数据,而第 32 天,我们使用了第 2 天到第 31 天的数据。第 2 天到第 30 天的数据是共同的。第 32 天的模型与第 31 天的模型不同之处在于它不考虑第 1 天丢失的所有数据点,但考虑了第 31 天丢失的数据点。除此之外,绝大多数数据(第 2 天到第 29 天)是共同的。因此,忽略这一点并每天从头开始训练整个模型似乎是浪费的。如果我们可以将方程(3)重新表述为对 X 的行 x_i 的某些函数的求和,我们可以在新数据到来时不断增加贡献,同时减去掉出滚动窗口的旧数据的贡献。

在第三章中我们讨论的矩阵乘法的众多解释之一可以帮助我们做到这一点。在第三章 的 III-B 节(大约动画 5)中,介绍了将矩阵乘法解释为两个矩阵行的外积之和。利用这一点,我们可以将方程 (3) 左侧的矩阵平方为:

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

在这里,向量 x_i 是矩阵 X 的第 i 行。它有一行和 m 列 (1m)。

同样,那个方程左侧的向量可以写成:

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

由于这两个项已经以 n 个数据点的总和表示,我们可以随意地添加或删除来自单个数据点的贡献。我们可以基于过去 7 天内的数据点,保持方程 (4) 中的矩阵和方程 (5) 中的向量的不断更新。如果某个数据点超出了 30 天的窗口,我们可以从两个方程中减去它对应的项,如果有新的数据点进入,我们可以将其项添加到两个方程中。而且由于 m 很小,我们可以高效地计算 𝛽,每次更新这些项时保持其最新。这种方法也可以用来为数据点添加权重。如果(例如)你从多个来源获取数据并希望对其中一些进行加权,这将非常有用。你只需将第 i 个数据点的权重 w_i 乘到方程 (4) 和 (5) 的每一项中。

IV) 神经网络

神经网络架构由向量层组成。第一层是输入层,最后一层是输出层,中间的所有层都是隐藏层。图片由 Midjourney 提供。

神经网络是受生物大脑神经连接启发的机器学习模型。它们是我们追求人工通用智能的当前选择武器。所有近期的进展,从文本到图像再到对话机器人,都使用了以神经网络为核心的模型。

线性回归可以被看作是最简单的神经网络。现在我们关注推断,即在给定模型输入实例的情况下获得输出向量的过程。对于线性回归,我们将获得一个行向量 x 作为输入和一个单一的标量值 y 作为输出。参数向量 𝛽 将输入转换为输出。

在线性回归中,我们得到一个对应于新数据点 x_j 的向量。这与参数向量 beta 相乘以产生响应 y_j,它是一个单一的标量。图片由作者提供。

要将其扩展到神经网络,我们以两种方式进行概括。

首先,为了使模型能够输出各种有趣的内容,如图像、句子、视频和宇宙飞船,我们需要它输出的不仅仅是一个标量(如线性回归),而是一个向量。如果这个向量的维度足够高,我们期望的任何复杂响应都可以有效地嵌入其相应的向量空间中。

因此,我们需要将线性回归的向量->标量情况改为向量->向量。标量输出只是一个特例,因为它是一个一维向量。这将上述图像更改为:

一般来说,我们希望响应 y 是一个向量而不是一个标量。这样,我们可以在那个向量空间中嵌入关于现实世界的复杂信息。图片由作者提供。

现在,之前的参数向量 𝛽 将需要变成参数矩阵。这是一个线性映射,将输入向量映射到输出向量。

但是,现实世界中大多数有趣的关系都是非线性的。为了适应这一点,我们首先插入了一堆中间的“隐藏层”,如下图中的蓝色部分所示。

深度神经网络的结构。图中的蓝色隐藏层是模型的中间层。图片由作者提供。

以这种方式添加层本身不会改变任何东西。它仍然等同于没有任何隐藏层的原始模型。要看到这一点,请注意,下面图中的参数矩阵 B_1, B_2, B_3, … 只是相乘并变成另一个参数矩阵。

B = (B_1. B_2. B_3.B_4)

但这个简单的技巧使得这种方法从只能任意逼近线性映射变成可以任意逼近任何映射。为了使隐藏层值得我们费心,我们在每一层添加一个简单的逐元素非线性函数 f。这是一个简单的一维函数 f,它接受一个标量作为输入并返回一个标量作为输出。我们只需在乘以下一个参数矩阵 B_j 之前,将 f 应用于向量的每个元素。这个 f 的一个流行选择是 Sigmoid 函数

通用逼近定理 [4] 说,这种架构可以任意逼近两个向量空间之间的任何映射(线性或非线性)。

V) 结论

谦逊的矩阵乘法是一种极其强大的工具。你会在最简单的模型以及最复杂、最前沿的模型中找到它。在本章中,我们回顾了一些以矩阵乘法为核心引擎的简单模型。在本书的后续章节中,我们将探索更多线性代数概念,突出它们在现代 AI 模型中的作用。

如果你喜欢这篇文章,给我买杯咖啡吧 😃 www.buymeacoffee.com/w045tn0iqw

参考文献

[1] 看待线性代数的美妙方式: medium.com/towards-data-science/a-beautiful-way-of-looking-at-linear-regressions-a4df174cdce

[2] 从最小二乘法推导线性回归: stats.stackexchange.com/questions/46151/how-to-derive-the-least-square-estimator-for-multiple-linear-regression

[3] 矩阵宝典: www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf

[4] 通用逼近定理: en.wikipedia.org/wiki/Universal_approximation_theorem

线性代数的鸟瞰图:基础知识

原文:towardsdatascience.com/a-birds-eye-view-of-linear-algebra-the-basics-29ad2122d98f

我们在思想上摆脱基础,但当真正需要时,我们会关上办公室的门,疯狂地计算矩阵。

Rohit PandeyTowards Data Science Rohit Pandey

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

--

线性代数的鸟瞰图。图像由 Midjourney 创建

这是正在进行中的线性代数书籍《线性代数的鸟瞰图》的第一章。目前的目录如下:

  1. 第一章:(当前)基础知识

  2. 第二章: 映射的度量——行列式

  3. 第三章: 为什么矩阵乘法是这样的?

  4. 第四章: 方程组、线性回归和神经网络

  5. 第五章: 秩与空秩及为什么行秩 == 列秩

线性代数是一门基础学科,它支撑了数学中可以做的一切。从物理学到机器学习、概率论(例如:马尔可夫链),无论你在做什么,线性代数总是潜伏在背后,一旦问题变得多维,它就会立刻出现。在我的经历中(我也从其他人那里听到过),这是高中和大学之间一个巨大的冲击源。在高中(印度),我接触了一些非常基础的线性代数(主要是行列式和矩阵乘法)。然后在大学的工程教育中,每个学科突然都假定你对特征值、雅可比矩阵等概念非常熟悉,好像你应该天生就具备这些知识一样。

这个博客旨在提供对这一学科中存在并且重要的概念及其明显应用的高层次概述。这样你至少知道自己不知道什么(如果有的话)。这也是一个收集资源和链接的借口,以便人们可以更深入地探索这个领域。

I) 向量空间

如前一节所述,当事物变成多维时,线性代数不可避免地出现。我们从一个标量开始,它只是某种数字。对于本文,我们将考虑这些标量的实数和复数。一般来说,标量可以是任何定义了加法、减法、乘法和除法基本操作的对象(抽象为“域”)。现在,我们需要一个框架来描述这样的数字集合(添加维度)。这些集合被称为“向量空间”。我们将考虑向量空间的元素是实数或复数的情况(前者是后者的特例)。得到的向量空间分别称为“实向量空间”和“复向量空间”。

线性代数中的概念适用于这些“向量空间”。最常见的例子是你的地板、桌子或你正在阅读本文的计算机屏幕。这些都是二维向量空间,因为你桌子上的每一个点都可以用两个数字来指定(如下面所示的 x 和 y 坐标)。这个空间被表示为 ,因为两个实数可以指定它。

我们可以以不同的方式推广 。首先,我们可以添加维度。我们生活的空间是三维的()。或者,我们可以对它进行曲线变换。例如,地球的表面(记作 )仍然是二维的,但与 (平坦的)不同,它是弯曲的。到目前为止,这些空间基本上都是数字的数组。但是向量空间的概念更为一般。它是一个对象集合,其中以下概念应该被定义得很明确:

  1. 任意两个对象的加法。

  2. 对象与标量(实数)的乘法。

不仅如此,这些对象还应该在这些操作下是“封闭”的。这意味着,如果你对向量空间的对象应用这两个操作,你应该得到相同类型的对象(你不应该离开向量空间)。例如,整数集合不是一个向量空间,因为通过标量(实数)乘法可能得到不是整数的结果(3*2.5 = 7.5,不是整数)。

表达向量空间中的对象的一种方式是使用向量。向量需要一个任意的“基”。基的一个例子是带方向的罗盘系统——北、南、东和西。任何方向(如“西南”)都可以用这些方向表示。这些是“方向向量”,但我们也可以有“位置向量”,其中我们需要一个原点和一个在该原点相交的坐标系统。地球表面的纬度和经度系统就是一个例子。纬度和经度对是一种标识你家位置的方法。但还有无限多种其他方式。另一种文化可能会将纬度和经度线绘制成与标准略有不同的角度,因此,他们会为你的房子得出不同的数字。但这并不会改变房子的实际位置。房子作为向量空间中的一个对象存在,这些表达位置的不同方式被称为“基”。选择一个基允许你为房子分配一对数字,而选择另一个基则允许你分配一组不同但同样有效的数字。

一个向量空间,其中每个位置都被组织并整齐地映射到一组数字。图像由 MidJourney 创建。

向量空间也可以是无限维的。例如,在[2]中的第 12 页,整个实数集被视为一个无限维的向量空间。

II) 线性映射

现在我们知道了什么是向量空间,让我们更进一步,讨论两个向量空间。由于向量空间只是对象的集合,我们可以考虑一种映射,将一个空间中的对象映射到另一个空间中的对象。一个例子是最近的 AI 程序,如 Midjourney,你输入一个文本提示,它们会返回一个匹配的图像。你输入的文本首先被转换为一个向量。然后,这个向量通过这样的“映射”转换为图像空间中的另一个向量。

VW 为向量空间(可以是实向量空间或复向量空间)。一个函数 f: V -> W 被称为“线性映射”,如果对于任何两个向量 u, v ∈ V 和任何标量 c(根据我们处理的是实向量空间还是复向量空间,c 可以是实数或复数),以下两个条件都满足:

f(u+v) = f(u) + f(v) __(1)

f(c.v) = c.f(v) __(2)

结合上述两个属性,我们可以得到关于n个向量的线性组合的以下结果。

f(c1.u1+ c2.u2+ … cn.un) = c1.f(u1)+c2.f(u2)+…+cn.f(un)

现在我们可以看到“线性映射”这个名称的来源。如果我们将一个 n 个向量的 线性组合 传递给线性映射 f(上面方程的左侧),这等同于将相同的线性映射应用于单个向量的函数 (f)。我们可以先应用线性映射,然后进行线性组合,或者先进行线性组合,然后应用线性映射。这两者是等价的。

在高中,我们学习线性方程。在二维空间中,这样的方程由 f(x)=m.x+c 表示。这里,mc 是方程的参数。请注意,这个函数不是线性映射。尽管它满足上面的方程 (1),但它未能满足方程 (2)。如果我们将 f(x)=m.x 代入,那么这是一个线性映射,因为它满足这两个方程。

线性映射将一个向量空间中的对象映射到另一个向量空间中的对象。就像是世界之间的一个传送门。当然,可能有许多这样的“映射”或“传送门”。线性映射必须满足其他属性。如果你将第一个空间中的向量的线性组合传递给它,先应用线性映射还是先应用线性组合都不重要。图像由 Midjourney 创建

III) 矩阵

在第一节中,我们介绍了向量空间的基的概念。给定第一个向量空间 (V) 的基和第二个向量空间 (U) 的维度,每个线性映射都可以表示为一个矩阵(详细信息见 这里)。矩阵只是向量的集合。这些向量可以按列排列,从而得到一个如下所示的二维数字网格。

矩阵作为按列排列的向量集合。图像由作者提供。

矩阵是人们在谈论线性代数时首先想到的对象,这有充分的理由。大多数时间花在练习线性代数上都是处理矩阵。但重要的是要记住,根据我们为第一个空间 V 选择的基,实际上有无数个矩阵可以表示一个线性映射。因此,线性映射的概念比用于表示它的矩阵要更加一般。

矩阵如何帮助我们执行它们表示的线性映射(从一个向量到另一个向量)?通过矩阵与第一个向量相乘。结果是第二个向量,映射完成(从第一个到第二个)。

具体来说,我们取第一个向量 v_1 与矩阵的第一行的点积(和积),这将得到结果向量 v_2 的第一个条目,然后是 v_1 与矩阵的第二行的点积以获得 v_2 的第二个条目,依此类推。下图展示了这一过程,对于一个具有 2 行和 3 列的矩阵。第一个向量 v_1 是三维的,第二个向量 v_2 是二维的。

矩阵与向量的乘法如何运作。图片由作者提供。

注意,具有这种维度 (2x3) 的矩阵背后的线性映射总是将一个三维向量 v_1 映射到二维空间 v_2

一种将三维空间中的向量映射到二维空间的线性变换。图片由 MidJourney 创建。

一般来说,一个 (nxm) 矩阵将一个 m 维向量映射到一个 n 维向量。

III-A) 矩阵的属性

让我们介绍一些矩阵的属性,这些属性将帮助我们识别它们所表示的线性映射的性质。

矩阵及其对应的线性映射的一个重要属性是秩。我们可以用一组向量来谈论这个问题,因为矩阵本质上就是一组向量。假设我们有一个向量,v1=[1,0,0]。这个向量的第一个元素是沿 x 轴的坐标,第二个元素是沿 y 轴的坐标,第三个元素是沿 z 轴的坐标。这三个轴是三维空间的基(有很多种基),,意味着这个空间中的任何向量都可以表示为这三个向量的线性组合。

一个三维空间中的单一向量。图片由作者提供。

我们可以将这个向量乘以一个标量,s。这会得到 s.[1,0,0] = [s,0,0]。当我们改变 s 的值时,我们可以得到沿 x 轴的任何一点。仅此而已。假设我们在集合中添加另一个向量,v2=[3.5,0,0]。那么,我们可以用这两个向量的线性组合得到哪些向量呢?我们将第一个向量乘以任何标量,s_1,第二个向量乘以任何标量,s_2。这给我们:

s_1.[1,0,0] + s_2[3.5,0,0] = [s_1+3.5 s_2, 0,0] = [s’,0,0]

这里,s’ 只是另一个标量。因此,即使对这两个向量进行线性组合,我们仍然只能到达 x 轴上的点。第二个向量并没有“扩展我们的到达范围”。用这两个向量的线性组合能到达的点数与用第一个向量能到达的点数完全相同。所以,即使我们有两个向量,这些向量的集合的秩仍然是1,因为它们跨越的空间是一维的。如果另一方面,第二个向量是 v2=[0,1,0],那么你可以用这两个向量在 x-y 平面上得到任何点。因此,所跨越的空间将是二维的,这个集合的秩将是2。如果第二个向量是 v2=[2.1,1.5,0.8],我们仍然可以用 v1v2 跨越一个二维空间(尽管这个空间与 x-y 平面不同,它将是其他的 2 维平面)。这两个向量的秩仍然是2。如果一个向量集合的秩与向量的数量相同(意味着它们可以一起跨越一个与向量数量相同的维度的空间),那么它们被称为“线性无关”。

如果构成矩阵的向量能够跨越一个m维空间,那么矩阵的秩就是m。但是,矩阵可以通过两种方式被看作是向量的集合。由于它是一个简单的二维数字网格,我们可以考虑所有的列作为向量组,或者考虑所有的行作为向量组,如下所示。这里,我们有一个(3x4)的矩阵(三行四列)。它可以被看作是 4 个列向量的集合(每个向量是 3 维的)或者 3 个行向量的集合(每个向量是 4 维的)。

矩阵可以被看作是行向量的集合或列向量的集合。图像由作者提供。

满行秩意味着所有的行向量是线性无关的。满列秩意味着所有的列向量是线性无关的。

当矩阵是方阵时,行秩和列秩将始终相同。这一点并不明显,相关证明可以在 mathexchange 帖子中找到,[3]。这意味着对于方阵,我们只需要讨论秩,而不必去指明“行秩”或“列秩”。

对于一个(3 x 3)矩阵,若其秩为 2,则对应的线性变换会将 3 维空间中的所有内容映射到一个较低的 2 维空间,就像我们在上一节中遇到的(3 x 2)矩阵一样。

一个光源将 3 维空间中点的阴影投射到 2 维地板或墙面上,这是一个将 3 维向量映射到 2 维向量的线性变换。图像由 MidJourney 创建。

与方阵的秩密切相关的概念是行列式和可逆性。

行列式

方阵的行列式在某种意义上是它的“测量”。让我通过回到将矩阵视为向量集合来解释。我们从一个向量开始。对它进行“测量”的方法是显而易见的——它的长度。由于我们只处理方阵,拥有一个向量的唯一方法是使它一维。实际上这就是一个标量。当我们从一维转到二维时,事情变得有趣了。现在,我们处于二维空间。因此,“测量”的概念不再是长度,而是面积。在这个二维空间中,两个向量形成的平行四边形的面积就是它们的行列式。如果这两个向量彼此平行(例如:都在 x 轴上)。换句话说,它们不是线性独立的,那么它们之间的平行四边形的面积将变为零。由它们形成的矩阵的行列式将为零,该矩阵的秩也将为零。

两个向量形成一个平行四边形。平行四边形的面积是由这两个向量构成的矩阵的行列式。图片由作者提供。

将维度提升一维,我们得到三维空间。要构造一个方阵(3x3),现在需要三个向量。由于三维空间中的“测量”概念是体积,因此一个(3x3)矩阵的行列式变成了构成它的向量之间的体积。

在三维空间中,需要三个向量来创建一个 3x3 矩阵。该矩阵的行列式是这些向量之间的体积。图片由 MidJourney 提供。

这可以扩展到任意维度的空间。

请注意,我们提到了向量之间的面积或体积。我们没有指定这些向量是构成方阵的行还是列。令人有些惊讶的是,我们不需要指定这一点,因为无论哪种方式都没有关系。无论我们取向量形成的行来测量它们之间的体积,还是取形成列的向量,我们都得到相同的答案。这在 mathexchange 的帖子 [4] 中得到了证明。

线性映射及对应矩阵的其他许多属性在理解它们并从中提取价值时非常宝贵。我们将在接下来的文章中深入探讨可逆性、特征值、对角化以及可以进行的各种变换(请回来查看链接)。

如果你喜欢这个故事,请请我喝杯咖啡 😃 www.buymeacoffee.com/w045tn0iqw

参考文献

[1] 线性映射: en.wikipedia.org/wiki/Linear_map

[2] Matousek 的小册子: kam.mff.cuni.cz/~matousek/stml-53-matousek-1.pdf

[3] 证明行秩与列秩相同的 Mathexchange 帖子:math.stackexchange.com/questions/332908/looking-for-an-intuitive-explanation-why-the-row-rank-is-equal-to-the-column-ran

[4] 证明矩阵及其转置的行列式相同的 Mathexchange 帖子:math.stackexchange.com/a/636198/155881

线性代数鸟瞰图:映射的度量——行列式

原文:towardsdatascience.com/a-birds-eye-view-of-linear-algebra-the-measure-of-a-map-determinant-1e5fd752a3be

Rohit PandeyTowards Data Science Rohit Pandey

·发布于 Towards Data Science ·阅读时间 16 分钟·2023 年 11 月 5 日

--

图像由 midjourney 创建

这是正在进行的线性代数书籍《线性代数鸟瞰图》的第二章。到目前为止的目录:

  1. 第一章: 基础

  2. 第二章: (当前)映射的度量——行列式

  3. 第三章: 为什么矩阵乘法是这样的?

  4. 第四章: 方程组、线性回归和神经网络

  5. 第五章: 秩与零度及行秩==列秩

线性代数是多维工具。不管你在做什么,只要你扩展到 n 维,线性代数就会出现。

前一章 中,我们描述了抽象的线性映射。在本章中,我们挽起袖子开始处理矩阵。实际考虑因素如数值稳定性、效率算法等将开始被探讨。

I) 如何量化线性映射?

前一章 中,我们讨论了向量空间的概念(基本上是 n 维数值集合——更广泛地说,是 的集合)和对这两个向量空间进行操作的线性映射。

作为这些映射的一种例子,一个向量空间可以是你坐的行星的表面,另一个可以是你可能坐的桌子的表面。从这个意义上说,世界地图也是地图,因为它们将地球表面的每一点“映射”到纸上或桌面上的一点,尽管它们不是线性映射,因为它们不保持相对面积(例如在某些投影中格林兰的面积看起来比实际大得多)。

地球表面的实际地图在线性代数的意义上也是一种地图,但它不是线性映射。图像由 midjourney 创建。

一旦我们为向量空间选择了一个 (空间中 n 个“独立”向量的集合;通常可以有无限的选择),所有在该向量空间上的线性映射都有唯一的矩阵与之对应。

目前,让我们将注意力限制在将向量从 n 维空间映射回 n 维空间的映射上(我们稍后会进行推广)。这些线性映射对应的矩阵是 n x n(见 第一章 的第 III 节)。将这种线性映射“量化”,以单一数字表示其对向量空间 R^n 的影响可能是有用的。我们处理的这种映射,实际上是将向量从 R^n 中“扭曲”到同一空间中的其他向量。原始向量 v 和映射转换后的向量 u 都有一些长度(例如 |v| 和 |u|)。我们可以考虑映射改变了向量长度的多少,|u|/|v|。也许这可以量化映射的影响?它对向量的“拉伸”程度?

这种方法有一个致命的缺陷。比率不仅依赖于线性映射,还依赖于它作用的向量 v。因此,它并不是线性映射本身的一个严格性质。

假如我们现在考虑两个向量,v_1v_2,它们通过线性映射转化为向量 u_1u_2。就像单个向量 v 的度量是它的长度一样,两个向量的度量是它们之间的平行四边形的面积。

由两个向量形成的平行四边形的面积。图像由 midjourney 创建。

就像我们考虑 v 长度变化的量一样,我们现在可以讨论 v_1v_2 之间的面积在通过线性映射后变成 u_1, u_2 时的变化量。遗憾的是,这同样不仅依赖于线性映射,还依赖于选择的向量。

接下来,我们可以扩展到三个向量,考虑它们之间的 平行六面体 体积的变化,并遇到相同的问题,即初始向量的影响。

三维空间中的一个三维区域。如果线性映射作用于这三个向量,无论初始选择的向量是什么,体积的变化量都相同。图像由 midjourney 创建。

但现在考虑原始向量空间中的一个 n 维区域。这个区域将具有某种“n 维度量”。要理解这一点,一个二维量度是面积(以平方公里计算)。三维量度是用于测量水的体积(以升计算)。四维量度在我们习惯的物理世界中没有对应物,但在数学上同样合理,是测量由四个 4-d 向量形成的平行六面体中四维空间的量,依此类推。

在 2 维空间中的度量是面积,在 3 维空间中的度量是体积。这些概念可以扩展到 4 维空间及以上。

n 个原始向量(v_1, v_2, …, v_n)形成一个平行六面体,该平行六面体被线性映射变换为n 个新向量,u_1, u_2 … u_n,它们形成自己的平行六面体。然后我们可以问新区域的 n 维度量与原始区域的关系。结果发现,这个比例确实只是线性映射的函数。无论原始区域的样子、位置等如何,线性映射作用于它后的度量与作用之前的度量之比将相同,是线性映射的纯粹函数。这个 n 维度量的比例(后/前)就是我们一直在寻找的。一个量化线性映射效果的唯一属性。

通过线性映射改变的任何n 维空间片段的比例是量化它对所作用空间影响的一个好方法。这个比例称为线性映射的行列式(这个名字的原因将在第 V 节中显现)。

目前,我们只是简单陈述了线性映射从 R^n 到 R^n“拉伸”任何 n 维空间片段的程度仅依赖于映射本身,而没有提供证明,因为这里的目的是激励。我们将在稍后(第 VI 节)提供证明,一旦我们准备好一些工具。

II) 计算行列式

那么,如何在给定一个从向量空间 R^nR^n 的线性映射时找到这个行列式呢?我们可以取任何n个向量,找到它们之间的平行六面体的度量,然后找到线性映射作用于所有向量后的新平行六面体的度量。最后,将后者除以前者。

我们需要使这些步骤更具体。首先,让我们开始在这个 R^n 向量空间中玩耍。

R^n 向量空间只是 n 个实数的集合。最简单的向量是 n 个零——[0, 0,…, 0]。这是零向量。如果我们用一个标量乘它,我们得到的仍然是零向量。这并不有趣。对于下一个最简单的向量,我们可以将第一个 0 替换为 1。这就得到了向量:e1=[1, 0, 0,.., 0]。现在,乘以一个标量 c 会得到一个不同的向量。

c.[1, 0, 0,.., 0] = [c, 0, 0, …, 0]

我们可以用 e1 “生成”无限多个向量,具体取决于我们选择的标量 c

如果 e1 是第一个元素为 1 其余为 0 的向量,那么 e2 是什么?第二个元素为 1 其余为 0 看起来是一个合乎逻辑的选择。

e2 = [0,1,0,0,…0]

将此推向逻辑的结论,我们得到一个由 n 个向量组成的集合:

e1 = [1, 0, 0,.., 0]

e2 = [0,1,0,0,…0]

e2 = [0,0,1,0,…0]

en = [0,0,0,0,…1]

这些向量构成了 R^n 向量空间的一个基底。这意味着什么?R^n 中的任何向量 v 都可以表示为这些 n 个向量的线性组合。这意味着对于某些标量 c1, c2,…, cn:

v = c1.e1+c2.e2+…+cn.en

所有向量,v 都是由向量集 e1, e2,…, en “生成”的。

这个特定的向量集合不是唯一的基底。任何 n 个向量的集合都可以。唯一的注意事项是,这些 n 个向量中没有一个应由其余向量“生成”。换句话说,所有的 n 个向量应当是线性无关的。如果我们从大多数连续分布中随机选择 n 个数,并重复这个过程 n 次来创建这 n 个向量,你将以 100%的概率获得一组线性无关的向量(在概率术语中为“几乎肯定”)。只是非常非常不可能随机向量恰好由其他 k<n 个随机向量“生成”。

回到本节开始时我们用来寻找线性映射行列式的公式,我们现在有一个基底来表示我们的向量。确定基底也意味着我们的线性映射可以表示为一个矩阵(参见第一章第 III 节)。由于该线性映射将向量从 R^n 映射回 R^n,因此相应的矩阵是 n x n

接下来,我们需要 n 个向量来形成我们的平行六面体。为什么不使用我们之前定义的 e1, e2, … en 标准基底。由这些向量包含的空间补丁的度量恰好是 1,这就是定义。下面的 图像希望能让这一点变得清晰。

向量 e1, e2, e3, …, en 之间包含的标准空间补丁。在这种情况下,我们有三个向量,因为空间是三维的。图像由 midjourney 创建。

如果我们将这些标准基底的向量收集成一个矩阵(行或列),我们会得到单位矩阵(主对角线上的 1,其他地方为 0):

单位矩阵。图像由作者提供。

当我们说可以将线性变换应用于任何 n 维空间的补丁时,我们也可以将其应用于这个“标准”补丁。

但,很容易证明将任何矩阵与单位矩阵相乘会得到相同的矩阵。因此,应用线性映射后的结果向量是表示线性映射本身的矩阵的列。因此,线性映射改变“标准补丁”体积的量与表示映射本身的矩阵的列向量之间的平行六面体的 n 维度量是相同的。

总结一下,我们从激发行列式作为线性映射改变 n 维空间补丁度量的比率开始。现在,我们展示了这个比率本身是 n 维度量。特别是,包含在任何表示线性映射的矩阵的列向量之间的度量。

III) 激发基本属性

我们在前一节中描述了线性映射的行列式应该简单地是任何其矩阵表示之间的度量。在这一节中,我们使用二维空间(其中度量是面积)来激发行列式必须具有的一些基本属性。

第一个属性是多重线性。行列式是一个将一组向量(收集在矩阵中)映射到一个标量的函数。由于我们限制在二维空间中,我们将考虑两个向量,都是二维的。我们的行列式(由于我们将其解释为向量之间平行四边形的面积)可以表示为:

det = A(v1, v2)

如果我们将一个向量添加到两个向量中的一个,函数应该如何表现?多重线性属性要求:

A(v1+v3, v2) = A(v1,v2)+A(v3,v2) __ (1)

从下面的动态图中可以看出这一点(注意新增的面积)。

行列式的加法属性。图片作者提供。

这个可视化也可以用于观察(通过缩放其中一个向量而不是添加另一个向量):

A(c.v1, v2) = c.A(v1, v2) __ (2)

第二个属性具有一个重要的含义。如果我们将负的 c 代入方程中,面积 A(v1, v2) 应该与 A(c.v1,v2) 的符号相反。

这意味着我们需要引入负面积和负行列式的概念。

如果我们接受负长度的概念,这就很有意义。如果一维空间中的长度度量可以是正的或负的,那么二维空间中的面积度量也应该允许为负。因此,任何维度的空间度量也应如此。

结合方程(1)和(2)就是多重线性属性。

另一个与行列式符号有关的重要属性是“交替属性”。它要求:

A(v1, v2) = -A(v2, v1) __(3)

交换两个向量的顺序会使行列式(或它们之间的度量)符号取反。如果你学过 叉积 3D 向量,这个性质会非常自然。为了说明这个性质,先考虑两个位置向量之间的一维距离 d(v1, v2)。显然 d(v1, v2) = -d(v2, v1),因为当我们从 v2 移动到 v1 时,我们的方向与从 v1 移动到 v2 时的方向相反。类似地,如果向量 v1v2 之间的面积是正的,那么 v2v1 之间的面积必须是负的。这个性质在 n 维空间中也成立。如果在 A(v1, v2, …, vn) 中我们交换两个向量,它会导致符号的改变。

交替性质还意味着如果其中一个向量只是另一个向量的标量倍数,则行列式必须是 0。这是因为交换两个向量应该会使行列式取反:

A(v1, v1) = -A(v1, v1)

=> 2 A(v1, v1) = 0

=> A(v1, v1) = 0

我们还可以通过多线性(公式 2):

A(v1, c.v1) = c A(v1, v1) = 0

从几何角度来看这是合理的,因为如果两个向量彼此平行,那么它们之间的面积是 0

视频 [6] 通过非常好的可视化展示了这些性质的几何动机,视频 [4] 也很好地可视化了交替性质。

IV) 代数推导:推导莱布尼茨公式

在这一部分,我们远离几何直观,从另一个角度,即冷冰冰的代数计算,来探讨行列式的话题。

如前一节通过几何学所激发的多线性和交替性质,(显著地)足以为我们提供一个非常具体的行列式代数公式,称为莱布尼茨公式。

这个公式帮助我们看到行列式的性质,这些性质从几何方法或其他代数公式中会非常困难。

莱布尼茨公式可以简化为拉普拉斯展开,涉及沿着某一行或列进行计算并计算余子式,这些在高中时许多人见过。

让我们推导莱布尼茨公式。我们需要一个函数,它接受矩阵的 n 列向量,𝛼1, 𝛼2, …, 𝛼n 作为输入,并将它们转换为一个标量 c

我们可以用空间的标准基表示每个列向量。

现在,我们可以应用多线性性质。暂时考虑第一个列,𝛼1

对第二列也可以这样做。我们只取上面求和中的第一个项,看看结果。

注意在第一个项中,我们得到向量e1出现两次。根据交替属性,该项的函数f变为0

为了让两个e1出现,积中两个a的第二个指标必须都变成1

所以,一旦我们对所有列进行这个操作,按照交替属性不会变为零的项将是那些第二个指标没有任何重复的项——即从1n的所有不同数字。换句话说,我们在寻找a的第二个指标中出现1n的排列。

那么,a的第一个指标是什么呢?这些指标只是从1n的顺序,因为我们首先提取a_1x,然后是a_2x,以此类推。用更简洁的代数符号表示,

在右边的表达式中,区域f(e_j1, e_j2, …, e_jn) 可以是 +1-10,因为e_j都是彼此正交的单位向量。我们已经确定,任何具有重复e_j的项将变为0,留下的只是排列(没有重复)。在这些排列中,我们有时会得到 +1,有时会得到 -1

排列的概念带有符号。这些区域的符号与排列的符号相等。如果我们用S_n表示{1,2,…,n}的所有排列集合,那么我们得到的就是行列式的莱布尼茨公式:

如果你对这个公式仍然感到困惑,可以查看mathexchange 帖子,[3]。如果你是那种需要看到实际代码才能理解的人,这里有一些 Python 代码:

import numpy as np
import itertools

def permut_sign(a):
    """
    The sign of a permutation is simply the number of inversions.
    This is a simple, O(n²) algorithm for finding the inversions. It can
    be done more efficiently with merge sort in O(n.log(n)) time.
    """
    cnt = 0
    for i in range(len(a)):
        for j in range(i+1, len(a)):
            if a[i] > a[j]:
                cnt += 1
    # Convert the 0-1 into -1 and +1
    return (cnt % 2 - .5) * -2

def leibniz_determinant(a):
    """
    Here, a is the matrix whose determinant we want to calculate.
    """
    n = len(a)
    arr = np.arange(n)
    determinant = 0
    for perm in itertools.permutations(arr):
        sign1 = permut_sign(perm)
        term = 1
        for i in range(len(perm)):
            j = int(perm[i])
            term = term * a[i, j]
        determinant += sign1 * term
    return determinant

a = np.array([[2, 1, 5],
              [4, 3, 1],
              [2, 2, 5]])

det1 = leibniz_determinant(a)
det2 = np.linalg.det(a)
print(abs(det1 - det2) <= 0.00001)

实际上,不应该使用这个公式来计算矩阵的行列式(除非只是为了娱乐或展示)。它是有效的,但由于涉及到所有排列的求和(即n!,超指数级),因此显得非常低效。

然而,行列式的许多理论性质在使用莱布尼茨公式时变得容易看出,而如果从它的其他形式开始,可能会很难解读或证明。例如:

  1. 使用这个公式可以明显看出,一个矩阵及其转置具有相同的行列式:|A| = |A^T|。这是公式对称性的简单结果。

  2. 可以使用与上述非常相似的推导来证明,对于两个矩阵 AB|AB| = |A|.|B|。参见这个答案mathexchange 帖子,[8]

  3. 使用莱布尼茨公式,我们可以很容易地看到,如果矩阵是上三角矩阵或下三角矩阵(下三角矩阵意味着矩阵对角线以上的每个元素都是零),行列式仅仅是对角线元素的乘积。这是因为除了一个排列:(a_11.a_22…a_nn)(主对角线),其他排列都得到一些零项或其他,从而使求和中的项为0

上三角矩阵。主对角线下方的所有元素都是 0。作者提供的图像。

第三个事实实际上导致了最有效的行列式计算算法,这是大多数线性代数库使用的。矩阵可以高效地分解为下三角矩阵和上三角矩阵(称为 LU 分解,我们将在下一章中介绍)。在完成这种分解后,第三个事实用于将这些下三角矩阵和上三角矩阵的对角线元素相乘以得到它们的行列式。最后,第二个事实用于将这两个行列式相乘以获得原始矩阵的行列式。

许多高中或大学的学生在首次接触行列式时,学习关于拉普拉斯展开的方法,包括按行或列展开,为每个元素找余因子并求和。这可以通过收集类似项从上面的莱布尼茨展开推导出来。参见这个答案中的mathexchange 帖子,[2]

V) 历史激发

行列式的历史激发。图像由 midjourney 创建。

行列式最初是在解决线性方程组的背景下发现的。假设我们有n个方程和n个变量(x_0, x_1, …, x_n)

线性方程组。作者提供的图像。

这个系统可以用矩阵形式表示:

矩阵形式的线性方程组。作者提供的图像。

更简洁地:

A.x = b

一个重要的问题是上述系统是否有唯一解x。行列式是一个“决定”这一点的函数。当且仅当A的行列式非零时,才存在唯一解。

这种历史性激发的方法使得行列式作为一个多项式,这个多项式在我们尝试解决与线性映射相关的线性方程组时出现。

更多内容请参见这个优质回答中的mathexchange 帖子,[8]

VI) 我们所激发的性质的证明

我们在本章开始时通过将行列式作为R^n 到 R^n 线性映射改变 n 维空间小块的度量来进行动机说明。我们还提到,这对 1、2、.. n-1 维度的度量不适用。下面是一个证明,我们使用了在其他部分遇到的一些性质。

如果你喜欢这本书,考虑请我喝杯咖啡以激励我 😃

www.buymeacoffee.com/w045tn0iqw

参考文献

[1] Mathexchange 帖子:线性映射的行列式与基的选择无关: math.stackexchange.com/questions/962382/determinant-of-linear-transformation

[2] Mathexchange 帖子:矩阵的行列式拉普拉斯展开(高中公式) math.stackexchange.com/a/4225580/155881

[3] Mathexchange 帖子:理解莱布尼茨公式的行列式 math.stackexchange.com/questions/319321/understanding-the-leibniz-formula-for-determinants#:~:text=The%20formula%20says%20that%20det,permutation%20get%20a%20minus%20sign.&text=where%20the%20minus%20signs%20correspond%20to%20the%20odd%20permutations%20from%20above.

[4] Youtube 视频:3B1B 关于行列式 www.youtube.com/watch?v=Ip3X9LOh2dk&t=295s

[5] 连接莱布尼茨公式与几何 math.stackexchange.com/questions/593222/leibniz-formula-and-determinants

[6] Youtube 视频:莱布尼茨公式即面积: www.youtube.com/watch?v=9IswLDsEWFk

[7] Mathexchange 帖子:行列式的乘积等于乘积的行列式 math.stackexchange.com/questions/60284/how-to-show-that-detab-deta-detb

[8] 行列式的历史背景: math.stackexchange.com/a/4782557/155881

线性代数的全景视角:为什么矩阵乘法是这样的?

原文:towardsdatascience.com/a-birds-eye-view-of-linear-algebra-why-is-matrix-multiplication-like-that-a4d94067651e

为什么第一个矩阵的列数必须与第二个矩阵的行数匹配?为什么不让两个矩阵的行数匹配呢?

Rohit PandeyTowards Data Science Rohit Pandey

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

--

图像由 midjourney 创建

这是正在进行的线性代数书籍《线性代数的全景视角》的第三章。到目前为止的目录:

  1. 第一章:基础知识

  2. 第二章:映射的度量 — 行列式

  3. 第三章:(当前)为什么矩阵乘法是这样的?

  4. 第四章:方程组、线性回归和神经网络

  5. 第五章:秩、零空间及为什么行秩 == 列秩

在这里,我们将描述我们可以用两个矩阵进行的操作,但要记住它们只是线性映射的表示。

I) 为什么关心矩阵乘法?

几乎任何信息都可以嵌入到向量空间中。图像、视频、语言、语音、生物识别信息以及你能想象的任何其他东西。所有的机器学习和人工智能应用(如最近的聊天机器人、文本转图像等)都基于这些向量嵌入。由于线性代数是处理高维向量空间的科学,它是一个不可或缺的基础。

现实世界中的复杂概念,如图像、文本、语音等,可以嵌入到高维向量空间中。向量空间的维度越高,它能编码的信息就越复杂。图像由 midjourney 创建。

许多技术涉及将某些输入向量从一个空间映射到其他空间的其他向量。

但为什么要关注“线性”,而大多数有趣的函数是非线性的?这是因为使模型高维和使其非线性(足够通用以捕捉各种复杂关系)的问题被发现是正交的。许多神经网络架构通过在它们之间使用简单的一维非线性线性层来工作。而且有一个定理表明,这种架构可以建模任何函数。

由于我们操作高维向量的方式主要是矩阵乘法,所以说它是现代人工智能革命的基石并不为过。

深度神经网络每一层都有向量,并且相邻层之间的连接以矩阵的形式编码。层间的转换通过线性代数和矩阵乘法完成。使用 midjourney 创建的图像。

II) 地图上的代数

使用 midjourney 创建的图像

第二章中,我们学习了如何通过行列式量化线性映射。现在,让我们对它们进行一些代数运算。我们需要两个线性映射和一个基。

II-A) 加法

如果我们可以加矩阵,我们可以加线性映射,因为矩阵是线性映射的表示。如果你了解标量加法,那么矩阵加法并不很有趣。就像向量一样,它仅在两个矩阵大小相同(行列相同)时定义,并涉及对齐它们并逐元素相加。

动画-0:两个矩阵的加法。图像由作者提供。

所以,我们只是在做一堆标量加法。这意味着标量加法的属性在逻辑上扩展。

交换律:如果你交换,结果不会改变

A+B = B+A

但上班通勤可能不是交换的,因为从 A 到 B 可能比从 B 到 A 花的时间更长。

结合律:在链中,不要犹豫,任选两个继续

A+(B+C) = (A+B)+C

恒等:我在这里回到起点!这对一个人来说可不公平!

存在一个特殊的元素,当它与任何东西相加时,结果还是原来的东西。对于标量来说,它是数字 0。对于矩阵来说,它是全零矩阵。

A + 0 = A 或 0 + A = A

同样,可以从任何元素开始,通过加法到达任何其他元素。因此,必须能够从 A 开始,到达加法单位 0。实现这一点所需加上的内容是 A 的加法逆元,称为 -A

A + (-A) = 0

对于矩阵,你只需访问矩阵中的每一个标量元素,并用每个元素的加法逆元替换(如果标量是数字,则切换符号)以获得矩阵的加法逆元。

II-B) 减法

减法实际上只是将第二个矩阵的加法逆元用于加法。

A-B = A+(-B)

II-C) 乘法

我们可以像定义矩阵加法一样定义矩阵乘法。只需取两个大小相同(行和列)的矩阵,然后逐元素相乘。对这种操作有一个名称,称为Hadamard 积

但实际上,我们将矩阵乘法定义为一种更复杂的操作,比加法更“异域”。它并不是为了复杂而复杂。它是线性代数中最重要的操作。

它享有这种特殊地位,因为它是将线性映射应用于向量的方式,建立在点积的基础上。

实际上它是如何工作的需要一个专门的部分来讨论,我们将在第 III 节中覆盖这一点。在这里,让我们列出一些它的属性。

交换律

与加法不同,矩阵乘法并不总是满足交换律。这意味着你应用线性映射到输入向量的顺序很重要。

A.B != B.A

结合律

它仍然是结合律的

A.B.C = A.(B.C) = (A.B).C

而且这个属性有很大的深度,我们将在第 IV 节中看到。

单位

与加法类似,矩阵乘法也有一个单位元素,I,当任何矩阵与其相乘时会得到相同的矩阵。大问题在于,这个元素仅存在于方阵中,而且它自身也是方阵。

由于矩阵乘法的重要性,通常所说的“单位矩阵”是指矩阵乘法的单位元素(而不是加法或 Hadamard 积的单位元素)。

加法的单位元素是由0构成的矩阵,而 Hadamard 积的单位元素是由1构成的矩阵。矩阵乘法的单位元素是:

矩阵乘法的单位矩阵以及线性代数的单位矩阵。图像由作者提供。

所以,主对角线上的1和其他地方的0。什么样的矩阵乘法定义会导致这样的单位元素?我们需要描述它是如何工作的,但首先让我们继续到最后的操作。

II-D) 除法

就像加法一样,单位矩阵的存在表明任何矩阵* A* 可以与另一个矩阵A^-1相乘并得到单位矩阵。这称为逆矩阵。由于矩阵乘法不是交换的,这里有两种方法来实现这一点。幸运的是,两者都导致单位矩阵。

A.(A^-1) = (A^-1).A = I

所以,“除以”一个矩阵其实就是与第二个矩阵的逆相乘,即 A.B^-1。如果矩阵乘法非常重要,那么这个操作也同样重要,因为它是逆操作。它也与我们如何历史性地发展(或许偶然发现)线性代数有关。不过这一点将在下一章(第四章)中讨论。

我们还将使用一个结合了加法和乘法的性质,即分配律。它适用于所有类型的矩阵乘法,从传统的乘法到 Hadamard 积:

A.(B+C) = A.B + A.C

III) 为什么矩阵乘法被定义成这样?

我们终于来到了本章的核心部分,回答标题中的问题。

矩阵乘法是线性映射作用于向量的方式。所以,我们可以这样来激励它。

III-A) 线性映射在实践中的应用是什么?

考虑一个线性映射,它将 m 维向量(来自 R^m)作为输入,并映射到 n 维向量(在 R^n 中)。我们将 m 维输入向量称为 v

在这一点上,考虑一下你自己实际在某种编程语言中编写这个线性映射的代码可能会有帮助。它应该是一个将 m 维向量 v 作为输入并返回 n 维向量 u 的函数。

# Code block-1: the skeleton of a linear map
import numpy as np

def linear_map(v, n):
  """
  A linear map that takes a vector, v as input and returns a vector, u
  n is the size of the output vector, u. For now, lets just return some
  random vector.
  """
  # get a vector u somehow. For now, let's just generate it.
  u = np.random.uniform(size=n)
  return u

线性映射必须以某种方式将这个向量转化为 n 维向量。在上面的函数中,你会注意到我们只是随机生成了一些向量。但这完全忽略了输入向量 v。这是不合理的,v 应该有所影响。现在,v 只是一个有序的 m 个标量的列表 v = [v1, v2, v3, …, vm]。标量的作用是什么?它们缩放向量。我们需要的输出向量应该是 n 维的。我们可以用一些(固定的)m 个向量(凭空抽取,每个都是 n 维的),w1, w2, …, wm。然后,将 w1v1 缩放,w2v2 缩放,依此类推,并将它们全部加起来。这就得到我们的线性映射的方程(输出在左边)。

Eq (1) 作为 m 个向量的线性组合激发的线性映射。图片由作者提供。

请注意上述方程(1),因为我们会再次使用它。

由于 w1, w2,… 都是 n 维的,所以 u 也是。并且 v=[v1, v2, …, vm] 的所有元素都对输出 u 产生影响。方程(1)的思想在下面得到实现。我们取一些随机生成的向量作为 w,但用固定的种子(确保每次调用函数时向量相同)。

# Code block-2: example of a linear map.
import numpy as np

def linear_map(v, n=3):
  """
  A linear map that takes a 2-d vector, v as input and returns 
  a 3-d vector, u
  """
  m = len(v)
  u = np.zeros(size=n)
  for i in range(m):
    # Fixing the seed ensures that the same m vectors are generated
    # every time the function is called. Ensuring it's the same linear
    # map.
    np.random.seed(i)
    # The linear map requires some fixed vectors, wi.
    wi = np.random.uniform(size=n)
    u = u + v[i]*wi
  return u

现在我们有一种方法可以将 m 维向量 (v) 映射到 n 维向量 (u)。但这个“映射”是否满足线性映射的性质?请回顾第一章第 II 节的线性映射性质,f(这里,ab 是向量,c 是标量):

f(a+b) = f(a) + f(b)

f(c.a) = c.f(a)

很明显,方程(1)所指定的映射满足上述两个线性映射的性质。

方程(1)中定义的函数满足一个属性,即函数的和是函数的和。图片来源:作者。

方程(1)中定义的函数满足一个属性,即标量乘以传递给函数的向量等同于标量乘以传递给函数的向量。图片来源:作者。

m个向量w1, w2, …, wm是任意的,无论我们选择什么,它们,方程(1)中定义的函数f都是一个线性映射。因此,对于这些w向量的不同选择会导致不同的线性映射。此外,对于你能想象的任何线性映射,都将有一些向量w1, w2,…可以与方程(1)一起应用来表示它。

现在,对于给定的线性映射,我们可以将向量w1, w2,…收集到一个矩阵的列中。这样的矩阵将具有n行和m列。这个矩阵表示了线性映射,f,它与输入向量v的乘积表示了线性映射f作用于v。这种应用就是矩阵乘法定义的来源。

动画(1):矩阵向量乘法。 图片来源:作者。

我们现在可以看到为什么矩阵乘法的单位元素是现在的样子:

动画(2)为什么乘法的单位矩阵是它现在的样子? 图片来源:作者。

我们从一个列向量v开始,并以一个列向量u结束(每个只有一列)。由于v的元素必须与表示线性映射的矩阵的列向量对齐,因此矩阵的列数必须等于v中的元素数。更多内容请参见第 III-C 节。

III-B) 矩阵乘法作为线性映射的组合

现在我们已经描述了矩阵如何与向量相乘,我们可以继续讨论矩阵与另一个矩阵的相乘。

当我们将矩阵视为线性映射的表示时,矩阵乘法的定义自然得多。

线性映射是将一个向量作为输入并产生一个向量作为输出的函数。假设与两个矩阵对应的线性映射是fg。你如何理解这些映射的加法(f+g)

(f+g)(v) = f(v)+g(v)

这让人想起了加法的分配律,其中参数进入括号内的两个函数,然后我们将结果相加。如果我们固定一个基,这对应于将两个线性映射应用于输入向量并将结果相加。根据矩阵和向量乘法的分配律,这等同于将对应于线性映射的矩阵相加,并将结果应用于向量。

现在,让我们考虑乘法(f.g)

(f.g)(v) = f(g(v))

由于线性映射是函数,乘法的最自然解释是将它们组合在一起(按顺序一个接一个地应用到输入向量上)。

当两个矩阵相乘时,结果矩阵表示相应线性映射的组合。考虑矩阵 A 和 B;积AB体现了先对输入向量应用由B表示的线性映射,然后再应用由A表示的线性映射所实现的变换。

所以我们有一个与矩阵A对应的线性映射,以及一个与矩阵B对应的线性映射。我们希望知道与这两个线性映射的组合对应的矩阵C。因此,首先对任意向量应用B,然后对结果应用A,应该等同于直接应用C

A.(B.v) = C.v = (A.B).v

在上一部分,我们学习了如何乘以矩阵和向量。让我们对A.(B.v)再做一次。假设B的列是列向量b1, b2, …, bm。从前一部分的方程(1)来看,

证明矩阵乘法只是顺序应用的线性映射。图片由作者提供。

如果我们直接将与C=A.B对应的线性映射应用到向量v上会怎样呢?矩阵C的列向量是c1, c2, …, ck

与动画(2)相同,矩阵与向量的乘法。图片由作者提供。

比较上述两个方程,我们得到,

方程(2):矩阵 C = AB 的列向量,其中 b1, b2,… 是 B 的列向量。图片由作者提供。

所以,乘积矩阵C=AB的列是通过对矩阵B的每一列应用与矩阵A对应的线性映射来获得的。将这些结果向量收集成一个矩阵就得到了C

我们刚刚将矩阵-向量乘法的结果扩展到了两个矩阵的乘法。我们只是将第二个矩阵分解为一个向量集合,将第一个矩阵应用到所有这些向量上,然后将结果向量收集到结果矩阵的列中。

方程(3):矩阵 C(C=A.B)的列向量。图片由作者提供。

所以结果矩阵C的第一行第一列是矩阵B的第一列与矩阵A的第一行的点积。一般而言,C的第i行第j列是矩阵A的第i行与矩阵B的第j列的点积。这是我们大多数人最初学习的矩阵乘法定义。

动画(3):矩阵乘法作为点积。图片由作者提供。

结合律证明

我们现在也可以展示矩阵乘法是结合的。用 AB 的乘积代替单一向量 v,将其应用于一组向量 w1, w2, …, wl。假设含有这些向量作为列向量的矩阵是 W。我们可以使用上面完全相同的技巧来展示:

(A.B).W = A.(B.W)

这是因为 (A.B).w1 = A.(B.w1),所有其他 w 向量也一样。

外积的和

假设我们在乘两个矩阵 AB

两个矩阵的乘法。图片由作者提供。

方程 (3) 可以推广为显示结果矩阵 Ci,j 元素是:

等式 (4):方程 (3) 的推广。图片由作者提供。

我们有对 k 项的求和。如果我们将每一项取出并创建 k 个单独的矩阵。例如,第一个矩阵将具有 i,j- 项:b_{i,1}. a_{1,j}。这 k 个矩阵及其与 C 的关系:

矩阵乘法是 k 个子矩阵的和。图片由作者提供。

k 个矩阵的求和过程可以如下可视化(类似于 III-A 节中将矩阵乘法可视化为向量的动画):

动画 (4):通过扩展到三维展示矩阵乘法。图片由作者提供。

我们在这里看到对 k 个相同大小 (nxm) 矩阵的求和,这些矩阵的大小与结果矩阵 C 相同。注意在等式 (4) 中,对于第一个矩阵 A,列索引保持不变,而对于第二个矩阵 B,行索引保持不变。所以我们得到的 k 个矩阵是 A 的第 i 列和 B 的第 i的矩阵乘积。

矩阵乘法作为外积的和。图片由作者提供。

在求和内部,两个向量相乘生成矩阵。这是一种应用于向量(矩阵的特殊情况)的矩阵乘法,称为“外积”。这里还有另一个动画展示这个外积和过程:

动画 (5):将矩阵乘法视为外积的和。图片由作者提供。

这告诉我们为什么 B 的行向量数量应该与 A 的列向量数量相同。因为它们必须一一对应以获得各自的矩阵。

我们已经看到很多可视化和一些数学内容,现在让我们通过代码查看相同的内容,特别是 AB 是方阵的情况。这基于《算法导论》第 4.2 节,[2]。

# Code block-3: naive matrix multiplication.
import numpy as np

def square_matrix_mult(a, b):
    """Multiply two matrices"""
    # The number of rows of a
    n = len(a)
    c = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            for k in range(n):
                c[i, j] = c[i, j] + a[i][k]*b[k][j]
    return c

a = [[1, 2, 3],
     [3, 4, 5],
     [2, 3, 4]]

square_matrix_mult(a, a)

III-C) 矩阵乘法:结构选择

图片由 midjourney 创建

矩阵乘法似乎以一种奇怪的方式结构化。很明显,我们需要进行一堆点积运算。因此,其中一个维度必须匹配。但是为什么要使第一个矩阵的列数等于第二个矩阵的行数呢?

如果我们重新定义矩阵,使得两个矩阵的行数(或列数)相同,这样会不会使事情变得更简单?这将使识别两个矩阵是否可以相乘变得更加容易。

传统定义中要求第一个矩阵的行与第二个矩阵的列对齐有多个优点。我们首先来看矩阵-向量乘法。第三节 A 中的动画(1)向我们展示了传统版本是如何工作的。让我们可视化一下,如果我们要求矩阵的行与向量中的元素数量对齐会是什么样的。现在,矩阵的 n 行将需要与向量的 n 元素对齐。

动画(6):矩阵乘法的替代设置会是什么样的?作者图片。

我们可以看到,我们必须以一个具有 n 行和一列的列向量 v 开始,最终得到一个具有 1 行和 m 列的行向量 u。这很尴尬,并且使得为矩阵乘法定义一个单位元素变得具有挑战性,因为输入和输出向量永远不能具有相同的形状。使用传统定义,这不是问题,因为输入是一个列向量,而输出也是一个列向量(见动画(1))。

另一个考虑因素是矩阵链的乘法。在传统方法中,很容易首先看到下面的矩阵链可以根据它们的维度进行相乘。

传统接受的方法下矩阵链乘法的样子。作者图片。

此外,我们可以确定输出矩阵将有 l 行和 p 列。

在要求两个矩阵的行对齐的框架下,这很快就会变得混乱。对于前两个矩阵,我们可以看出行应该对齐,并且结果将会有 n 行和 l 列。但是,想象一下结果会有多少行和列,然后再推断它是否与 C 兼容等,就成了一场噩梦。

如果我们修改矩阵乘法的定义,矩阵链乘法可能会是什么样的。作者图片。

这就是为什么我们要求第一个矩阵的行与第二个矩阵的列对齐的原因。但也许我遗漏了什么。也许有一个“更干净”的替代定义能够绕过这两个挑战。欢迎在评论中分享想法 😃

III-D) 矩阵乘法作为基变换

到目前为止,我们把矩阵乘法与向量视为一种线性映射,它接受一个向量作为输入,并返回另一个向量作为输出。但矩阵乘法还有另一种思考方式——作为一种改变视角的方式。

让我们考虑二维空间 。我们用两个数字来表示这个空间中的任何向量。这些数字代表什么?它们分别是沿 x 轴和 y 轴的坐标。一个指向 x 轴的单位向量是 [1,0],一个指向 y 轴的单位向量是 [0,1]。这些是我们空间的基底。现在每个向量都有了一个地址。例如,向量 [2,3] 意味着我们将第一个基底向量缩放 2 倍,第二个基底向量缩放 3 倍。

但这并不是空间的唯一基底。其他人(比如,那位不可提及者)可能想使用另外两个向量作为他们的基底。例如,向量 e1=[3,2]e2=[1,1]。空间 中的任何向量也可以用他们的基底来表示。相同的向量在我们的基底和他们的基底下会有不同的表示。就像同一所房子的不同地址(可能基于不同的邮政系统)。

当我们在那位不可提及者的基底下时,向量 e1 = [1,0] 和向量 e2 = [0,1] (这是按基底向量的定义,从他的视角看是基底向量)。将向量从我们的基底系统转换到那位不可提及者的基底系统以及反向转换的函数是线性映射。因此,这些转换可以表示为矩阵乘法。我们将将向量从我们这里转换到那位不可提及者的向量的矩阵称为 M1,而执行相反操作的矩阵称为 M2。我们如何找到这些矩阵呢?

改变你看待世界的视角。图像由 midjourney 提供。图像由作者提供。

我们知道,我们称之为 e1=[3,2]e2=[1,1] 的向量,而那位不可提及者称 e1=[1,0]e2=[0,1]。让我们将我们版本的向量收集到一个矩阵的列中。

由两个列向量组成的基本 2 乘 2 矩阵。图像由作者提供。

并将那位不可提及者的向量 e1e2 收集到另一个矩阵的列中。这只是单位矩阵。

我们希望将 2 乘 2 矩阵转换为单位矩阵以改变基底。图像由作者提供。

由于矩阵乘法对第二个矩阵的列进行独立操作,

将矩阵移到单位矩阵的方程。图像由作者提供。

在两侧乘以适当的矩阵得到 M1:

用矩阵改变基底。图像由作者提供。

反向操作得到 M2:

反向映射到基底。图像由作者提供。

这可以概括为以下陈述:一个列向量为 w1, w2, …, wn 的矩阵将以 w1, w2, …, wn 为基向量的基中表达的向量转换到我们的基中。

而该矩阵的逆矩阵将向量从我们的基变换到* w1, w2, …, wn* 为基的那个基。

因此,所有方阵可以被视为“基变换器”。

注意:在特殊情况下,正交归一矩阵(每列是单位向量且与其他列正交)的逆矩阵与其转置矩阵相同。因此,将矩阵的列作为基变换就等同于将一个向量与每一行进行点积。

欲了解更多信息,请参见 3B1B 视频,[1]

结论

矩阵乘法可以说是现代计算和几乎所有数据科学领域中最重要的操作之一。深入理解其工作原理对任何数据科学家来说都非常重要。大多数线性代数教材描述了“是什么”,但没有解释其结构的原因。希望这篇博客填补了这个空白。

参考文献

[1] 关于基变换的 3B1B 视频: www.youtube.com/watch?v=P2LTAUO1TdA&t=2s

[2] 《算法导论》,作者 Cormen 等,第三版

[3] 矩阵乘法作为外积之和: math.stackexchange.com/questions/2335457/matrix-at-a-as-sum-of-outer-products

[4] 卡塔兰数维基百科条目 en.wikipedia.org/wiki/Catalan_number

语言模型简史

原文:towardsdatascience.com/a-brief-history-of-language-models-d9e4620e025b?source=collection_archive---------11-----------------------#2023-05-12

通向 GPT 的突破——为非专家解释

Dorian DrostTowards Data Science Dorian Drost

·

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

--

今天大型语言模型如 GPT 在媒体上获得的压倒性关注,给人一种我们正身处持续革命中的印象。然而,即使是革命也是建立在其前身的成功基础上的,而 GPT 是数十年研究的结果。

在这篇文章中,我想概述一下在语言模型研究领域中一些重要的发展步骤,这些步骤最终导致了我们今天所拥有的大型语言模型。我将简要描述语言模型的一般概念,然后讨论在不同时间主导该领域的一些核心技术,这些技术通过克服其前身的障碍和困难,为今天的技术铺平了道路,其中(Chat-)GPT 可能是最著名的代表。

什么是语言模型?

将单词转化为语言模型所需的条件是什么?照片由Glen Carrie提供,来自Unsplash

语言模型是一种机器学习模型,给定一系列单词,预测下一个单词。就是这么简单!

主要思想是,这样的模型必须具有人类语言的某种表示。在某种程度上,它模拟了我们语言所依赖的规则。经过看过数百万行文本后,模型将表示这样一个事实:像动词、名词和代词这样的事物在语言中存在,并且它们在句子中有不同的功能。它可能还会得到一些来自单词含义的模式,比如“巧克力”经常出现在“甜的”、“糖”和“脂肪”等单词的语境中,但几乎不会与“割草机”或“线性回归”等单词一起出现。

正如提到的,它通过学习给定一系列单词预测下一个单词的方式得到这种表示。这是通过分析大量文本来推断在给定上下文中下一个可能的单词。让我们看看如何实现这一点。

初学者

在我们可以考虑更复杂的技术之前,我们必须从简单的开始。照片由Jon Cartagena提供,来自Unsplash

让我们从一个直觉上的第一个想法开始:给定大量的文本,我们可以计算在给定上下文中每个单词的频率。上下文仅仅是前面出现的单词。也就是说,例如,我们统计词语“like”在词语“I”之后出现的频率,统计它在词语“don’t”之后出现的频率,依此类推,对于所有在词语“like”之前出现过的单词。如果我们将这个频率除以前面的词语的频率,我们很容易得到概率 P(“like” | “I”),读作给定词语“I”,单词“like”的概率:

P(“like” | “I”) = count(“I like”) / count(“I”)

P(“like” | “don’t”) = count(“don’t like”) / count(“don’t”)

我们可以为我们在文本中找到的每对单词做到这一点。然而,存在一个明显的缺点:用于确定概率的上下文仅仅是一个单词。这意味着,如果我们想预测在单词“don’t”之后出现的内容,我们的模型不知道“don’t”之前是什么,因此无法区分“他们不”,“我不”或“我们不”。

要解决这个问题,我们可以扩展上下文。所以,不再计算 P(“like” | “don’t”),而是计算 P(“like” | “I don’t”)、P(“like” | “they don’t”)、P(“like” | “we don’t”) 等等。我们甚至可以将上下文扩展到更多单词,这就是我们称之为 n-gram 模型,其中 n 表示要考虑的上下文单词数量。一个 n-gram 只是 n 个单词的序列,因此,“I like chocolate” 是一个 3-gram。

n 越大,模型在预测下一个单词时能考虑的上下文就越多。然而,n 越大,我们需要计算的不同概率就越多,因为例如 5-gram 比 2-gram 要多得多。不同的 n-gram 数量呈指数增长,并且很容易达到一种无法处理它们的内存或计算时间的程度。因此,n-gram 只允许我们非常有限的上下文,这对于许多任务来说是不够的。

循环神经网络

循环神经网络一遍又一遍地执行相同的步骤。照片由 Önder Örtel 提供,来自 Unsplash

循环神经网络(RNNs)引入了一种方法来解决 n-gram 模型在处理更大上下文时遇到的问题。在 RNN 中,输入序列逐个单词地被处理,生成所谓的 hidden representation。其主要思想是,这个隐藏表示包含到目前为止序列的所有相关信息,并且可以在下一步中用于预测下一个单词。

让我们举个例子:假设我们有这样一个句子

The mouse eats the cheese

现在,RNN 逐个处理单词(首先是“mouse”,然后是“eats”,...),创建隐藏表示,并预测最可能出现的下一个单词。例如,当我们到达单词“the”时,模型的输入将包括当前单词(“the”)和一个隐藏表示向量,该向量包含了句子“the mouse eats”的相关信息。这些信息用于预测下一个单词(例如“cheese”)。请注意,模型看不到单词“the”、“mouse”和“eats”;它们被编码在隐藏表示中。

这是否比 n-gram 模型看到最后的 n 个词更好?嗯,这要看情况。隐藏表示的主要优点在于,它可以包含关于不同大小序列的信息,而不会呈指数增长。在一个 3-gram 模型中,模型准确地看到 3 个词。如果这不足以准确预测下一个词,它无能为力,因为没有更多的信息。另一方面,RNN 使用的隐藏表示包括整个序列。然而,它必须以某种方式将所有信息适配到这个固定大小的向量中,因此信息不会以逐字方式存储。如果序列变得更长,这可能会成为一个瓶颈,所有相关信息都必须经过这个固定大小的向量。

你可以这样理解它们之间的区别:n-gram 模型只看到有限的上下文,但它清晰地看到这个上下文(词本身),而 RNN 具有更大且更灵活的上下文,但它只看到一个模糊的图像(隐藏表示)。

不幸的是,RNN 还有另一个缺点:由于它们一个词一个词地处理序列,因此无法并行训练。为了处理位置 t 的词,你需要步骤 t-1 的隐藏表示,而步骤 t-1 的隐藏表示又需要步骤 t-2 的隐藏表示,以此类推。因此,无论是在训练还是推理过程中,计算都必须一个步骤一个步骤地进行。如果可以并行计算每个词所需的信息,不是更好吗?

注意力机制的救援:变换器

注意力全在于击中正确的位置。照片来自Afif RamdhasumaUnsplash

变换器是一类解决 RNN 缺点的模型。它们避免了隐藏表示的瓶颈问题,并且允许并行训练。它们是如何做到的呢?

变换器模型的关键组件是注意力机制。请记住,在 RNN 中,存在一个包含到目前为止输入序列所有信息的隐藏表示。为了避免单一表示带来的瓶颈,注意力机制在每一步构建一个新的隐藏表示,该表示可以包含来自任何先前词的信息。这使得模型能够决定序列中的哪些部分对于预测下一个词是相关的,因此可以通过为这些部分分配更高的相关性来专注于它们,从而计算下一个词的概率。假设我们有一个句子

当我前几天看到多萝西和稻草人时,我走到她面前说:“嗨

我们希望预测下一个词。注意力机制使模型能够专注于与续写相关的词,并忽略那些不相关的部分。在这个例子中,代词“her”必须指代“Dorothy”(而不是“the scarecrow”),因此模型必须决定专注于“Dorothy”并忽略“the scarecrow”来预测下一个词。对于这个句子,更可能的续写是“Hi, Dorothy”而不是“Hi, scarecrow”或“Hi, together”。

RNN 只会有一个隐藏表示向量,这个向量可能包含也可能不包含决定代词“her”指代谁所需的信息。相比之下,利用注意力机制,创建了一个新的隐藏表示,其中包含来自词“Dorothy”的大量信息,但来自其他当前不相关的词的信息较少。对于下一个词的预测,将再次计算新的隐藏表示,这可能看起来非常不同,因为现在模型可能希望更多地关注其他词,例如“scarecrow”。

注意力机制还有一个优点,即它允许训练的并行化。如前所述,在 RNN 中,你必须一个接一个地计算每个词的隐藏表示。在 Transformer 中,你在每一步计算隐藏表示,仅需单个词的表示。特别是,对于计算第 t 步的隐藏表示,你不需要第 t-1 步的隐藏表示。因此,你可以同时计算两者。

近年来模型规模的增加,使得模型每天的表现越来越好,这仅仅是因为技术上实现了这些模型的并行训练。使用循环神经网络,我们无法训练具有数百亿参数的模型,因此无法利用这些模型与自然语言交互的能力。Transformer 的注意力机制可以看作是最后一个组成部分,它与大量的训练数据和足够的计算资源一起,为开发像 GPT 及其兄弟模型并启动人工智能和语言处理的持续革命提供了必要条件。

总结

那么,在这篇文章中我们看到了什么?我的目标是给你一个概述,介绍达到今天强大语言模型所必需的一些主要步骤。作为总结,以下是按顺序排列的重要步骤:

  • 语言建模的关键方面是给定一段文本序列来预测下一个词。

  • n-gram 模型只能表示有限的上下文。

  • 循环神经网络具有更灵活的上下文,但它们的隐藏表示可能成为瓶颈,并且无法并行训练。

  • Transformer 通过引入注意力机制来避免瓶颈,这使得能够详细关注上下文的特定部分。最终,它们可以并行训练,这对于训练大型语言模型是必需的。

当然,为了达到我们今天拥有的模型,还需要许多其他技术。这一概述只是突出了一些非常重要的关键方面。你认为在迈向大型语言模型的过程中,还有哪些步骤是相关的?

进一步阅读

欲了解更多背景和技术细节,可以查看被称为语言建模圣经的书籍:

以下论文介绍了迈向大型语言模型过程中的一些里程碑。

循环神经网络:

  • Elman, J. L. (1990). Finding structure in time. 认知科学, 14(2), 179–211.

  • Hochreiter, S., & Schmidhuber, J. (1997). Long short-term memory. 神经计算, 9(8), 1735–1780.

Transformer:

  • Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … & Polosukhin, I. (2017). Attention is all you need. 神经信息处理系统进展, 30.

喜欢这篇文章? 关注我 以便收到我未来的帖子通知。

神经网络简介:一个分类问题

原文:towardsdatascience.com/a-brief-introduction-to-neural-networks-a-classification-problem-43e68c770081

实用的 Python 神经网络初学者指南

Chayma ZatoutTowards Data Science Chayma Zatout

· 发布于 Towards Data Science · 18 分钟阅读 · 2023 年 1 月 24 日

--

图片来源 W TUnsplash

上一篇教程中,我介绍了神经网络的基础,并提供了一个简单的回归问题示例。我简要概述了使用神经网络的一般过程。在本教程中,我们将进一步深入学习如何将神经网络用于分类任务。我们将遵循之前相同的一般流程。然而,如果你需要更多关于神经网络的背景信息,我建议你回顾上一篇教程,在其中我也简要讨论了神经元和多层网络的概念。

目录

· 1. 介绍

· 2. 问题理解

· 3. 数据准备与预处理

∘ 3.1. 数据描述

∘ 3.2. 数据转换

· 4. 模型构思

∘ 4.1. 单个单元输出

∘ 4.2. 单热输出

∘ 4.3. 卷积神经网络

· 5. 训练

∘ 5.1. 单个单元输出

∘ 5.2. 单热输出

∘ 5.3. 卷积单元

· 6. 验证

∘ 6.1 预测

∘ 6.2. 学习曲线

∘ 6.3. 测试集评估

∘ 6.4. 评估指标

∘ 6.5. 显示一些数据

· 7. 结论

1. 介绍

正如之前讨论的,机器学习解决方案的目标是开发一个能够通过分析为特定任务创建的数据集来生成所需输出的模型。为实现这一目标,必须遵循一系列步骤,包括:

  • 问题理解。

  • 数据准备和预处理。

  • 模型构思。

  • 训练模型。

  • 模型评估和验证

2. 问题理解

在讨论我们将在本教程中解决的分类问题之前,理解有几种分类类型是很重要的。具体来说:

  • 二分类,当类别数量为两个时,例如将电子邮件分类为垃圾邮件或非垃圾邮件。

  • 多类分类,当有两个以上不同类别时,例如在Iris 数据集中,类别是不同类型的花。

  • 多标签分类,当输入有多个类别时,例如将包含多个对象的图像进行分类。

  • 此外,如果输入是图像,当每个像素都有自己的类别时,分类可以是逐像素分类(或图像分割)。

了解分类类型有助于我们选择合适的模型类型和适当的训练参数,例如损失函数。例如,对于二分类,通常使用binary_crossentropy函数作为损失函数,而对于多类分类,则使用categorical_crossentropy

我们在本教程中要解决的问题是手写数字分类(多类分类)。换句话说,给定一个手写数字作为输入(从 0 到 9),模型必须识别它并给出写了什么数字作为输出。我们将测试三种类型的模型:一个基本的直接神经网络、一个输出进行过 one-hot 编码的基本直接神经网络和一个卷积神经网络(CNN)。

让我们从导入所需的库开始:

from tensorflow.python.keras import Input
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense, Dropout, Conv2D, MaxPooling2D, Flatten
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import accuracy_score, precision_score, recall_score, ConfusionMatrixDisplay, f1_score
from sklearn.model_selection import train_test_split
import os

并设置种子,以便我们可以重新生成结果:

from numpy.random import seed
seed(1)

from tensorflow import random, config
random.set_seed(1)
config.experimental.enable_op_determinism()

import random
random.seed(2)

3. 数据准备和预处理

为了训练我们的模型,我们将使用 MNIST 数据集,该数据集包括 60,000 个训练样本和 10,000 个测试样本。如果您希望使用原始的 IDX 格式数据集,可以查看我的教程,以便轻松探索它。或者,您可以直接使用 Keras 提供的数据集,如下所示:

# read dataset:
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

3.1. 数据描述

我们从显示数据形状开始:

print(f"The training data shape: {x_train.shape}, its label shape: {y_train.shape}")
print(f"The test data shape: {x_test.shape}, its label shape: {y_test.shape}")
The training data shape: (60000, 28, 28), its label shape: (60000,)
The test data shape: (10000, 28, 28), its label shape: (10000,)

单个样本是一个通道的图像(灰度图像),形状为 28×28 像素。显示图像中像素值的范围也很重要,以确定是否需要后续的数据缩放。

print("Minimum value:", np.min(x_train[0]))
print("Maximum value:", np.max(x_train[0]))
Minimum value: 0
Maximum value: 255

确实,后续需要进行数据缩放。

另一个重要因素是显示每个类别中的样本数量。这对于确定是否面对数据不平衡非常重要:

# Display bars:
fig, axs = plt.subplots(1, 2)
unique, counts = np.unique(y_train, return_counts=True)
axs[0].bar(unique, counts, width=0.4)
axs[0].set_title('Train set')
unique, counts = np.unique(y_test, return_counts=True)
axs[1].bar(unique, counts, width=0.4)
axs[1].set_title('Test set')
plt.show()

数据不平衡是机器学习中的一个问题,其中不同类别之间样本数量差异显著。在我们的案例中,样本数量大致相同。

3.2. 数据转换

数据转换是数据预处理技术之一。它包括:数据归一化、数据编码、填补缺失值的数据插补、将连续特征转化为分类特征的数据离散化,以及减少数据集中特征数量的降维。对于这个例子,我们将只应用数据归一化和数据编码,因为没有缺失值,也不需要数据离散化和降维。

  • 数据归一化:是一种将数据集特征的值转换到特定范围内的技术。这通常是为了确保数据在适合神经网络或其他机器学习方法的范围内。我们将像素归一化到[0,1]的范围:
# Scale images to the [0, 1] range:
x_train = x_train.astype("float32") / 255
x_test = x_test.astype("float32") / 255
  • 重新塑形(从 2D 图像到行图像):图像重新塑形或展平是一种常见的数据转换技术,将空间信息转换为一行像素。这是输入到一些机器学习模型(如多层感知器(MLP)或线性回归)之前的必要步骤。

我们为前两个模型重新塑形图像:

# Data reshaping : from 2D image to row image
x_train = x_train.reshape((x_train.shape[0], x_train.shape[1] * x_train.shape[2]))
x_test = x_test.reshape((x_test.shape[0], x_test.shape[1] * x_test.shape[2]))
  • 数据编码:一热编码是一种用于用二进制向量表示有限类别数量的分类变量的技术。对于一组 n 个标签,每个标签由长度为 n 的向量表示,其中每个元素为 0,除了对应标签的元素为 1。在我们的案例中,我们要预测的变量是从 0 到 9 的数字类别,这是一个可以用一热编码表示的有限类别。例如:

一热编码将用于第二和第三模型的输出:

# One-hot encoding:
y_train = tf.keras.utils.to_categorical(y_train, num_classes=10)
y_test = tf.keras.utils.to_categorical(y_test, num_classes=10)
  • 重新塑形(以扩展维度)。 重新塑形以扩展维度通常在 CNN 中使用,以增加输入图像中的通道数:
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)
print(f"The training data shape: {x_train.shape}, its label shape: {y_train.shape}")
print(f"The test data shape: {x_test.shape}, its label shape: {y_test.shape}")
The training data shape: (60000, 28, 28, 1), its label shape: (60000, 10)
The test data shape: (10000, 28, 28, 1), its label shape: (10000, 10)

总结数据处理:对所有模型的输入应用数据缩放,对输入从 2D 图像转换为展平图像(行表示)应用于第一个和第二个模型,因为它们不是基于 CNN 的,对第二个和最后一个模型的输出应用一热编码,最后,仅对基于 CNN 的模型扩展输入,以使输入图像变为大小为 28×28 像素的单通道输入。

在预处理阶段结束时,训练集被分为训练集和验证集:

# Split dataset:
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.3, random_state=42)

我们的数据准备好用于训练,但在此之前,我们需要构建我们的模型。

4. 模型构思

现在,我们将演示三种不同模型的实现,首先是一个简单的全连接层模型,然后逐步改进。在本教程中,我们将重点介绍前一个教程中未涵盖的基础知识,例如人工神经元模型、激活函数、层和多层模型。

4.1. 单一输出单元

第一个模型是一个完全连接层的序列,最后跟随一个单一的输出单元。这个模型类似于前一个教程中使用的模型。它易于实现,并且可以为这个特定示例产生良好的结果。

现在我们已经概述了模型的一般架构,我们需要考虑如何根据前一个教程中讨论的可用激活函数来预测输入类别(标签)。这些函数都返回实数,但我们可以使用它们并将预测的数字四舍五入为整数。然而,我们需要确保最终层中激活函数的输出范围包含所有可能的类别值。因此,像sigmoidtanhsoftsign这样的函数在这种情况下不能使用。

带有单一输出以预测输入标签的全连接神经网络

让我们创建我们的模型!它将有 5 个隐藏层,每层有 224 个单元,并使用sigmoid激活函数。输出层将有一个单元,并使用relu激活函数。

# Create model:
model = Sequential()
model.add(Input(shape=(train_x.shape[1],)))
model.add(Dense(224, activation='sigmoid'))
model.add(Dense(224, activation='sigmoid'))
model.add(Dense(224, activation='sigmoid'))
model.add(Dense(224, activation='sigmoid'))
model.add(Dense(224, activation='sigmoid'))
model.add(Dense(1, activation='relu'))
print(model.summary())

模型总结:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 224)               175840    
_________________________________________________________________
dense_1 (Dense)              (None, 224)               50400     
_________________________________________________________________
dense_2 (Dense)              (None, 224)               50400     
_________________________________________________________________
dense_3 (Dense)              (None, 224)               50400     
_________________________________________________________________
dense_4 (Dense)              (None, 224)               50400     
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 225       
=================================================================
Total params: 377,665
Trainable params: 377,665
Non-trainable params: 0
_________________________________________________________________

4.2. 一热输出

虽然前面的模型产生了良好的结果,但正如我们稍后看到的那样,通过一个更小的模型可以获得更好的结果。关键的区别在于输出层使用了一热编码。通常,在机器学习中,一热编码是通过具有 n 个单元的密集层实现的(其中 n 是可能的类别数量),并且使用softmax激活函数。

嗯,我不确定前一个教程中是否定义了softmax,那么softmax是什么呢?

“Softmax 函数将值向量转换为概率分布。输出向量的元素在(0, 1)范围内,并且总和为 1。Softmax 通常用作分类网络最后一层的激活函数,因为结果可以解释为概率分布。” [1]

总体模型具有以下架构:

带有分类输出的全连接神经网络

为了创建我们的模型,我们将首先定义 dropout 层,这是一种正则化技术,用于防止过拟合(稍后会解释):

“Dropout 层在训练期间以rate的频率随机将输入单元设置为 0,这有助于防止过拟合。请注意,Dropout 层仅在训练设置为 True 时应用,因此在推断过程中不会丢失任何值。” [2]

在 Keras 中,dropout 层定义如下,其中rate是一个介于 0 和 1 之间的浮点数,表示要丢弃的输入单元的比例:

keras.layers.Dropout(rate, **kwargs)

现在,让我们创建我们的模型!它将有 2 个隐藏层,每个隐藏层有 224 个单元,并使用 relu 激活函数。将添加一个具有 10 个单元和 softmax 激活函数的密集输出层。在每个密集隐藏层后添加一个 dropout 层以防止过拟合。

# Create model:
model = Sequential()
model.add(Input(shape=(train_x.shape[1],)))
model.add(Dense(224, activation='relu'))
model.add(Dropout(rate=0.4))
model.add(Dense(224, activation='relu'))
model.add(Dropout(rate=0.4))
model.add(Dense(10, activation='softmax'))
print(model.summary())

模型摘要:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 224)               175840    
_________________________________________________________________
dropout (Dropout)            (None, 224)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 224)               50400     
_________________________________________________________________
dropout_1 (Dropout)          (None, 224)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                2250      
=================================================================
Total params: 228,490
Trainable params: 228,490
Non-trainable params: 0
_________________________________________________________________

如你所见,此模型相比于之前的模型(377,665 个参数)具有较少的参数,你会发现它能提供更好的结果。

4.3. 卷积神经网络

到目前为止,我们将图像视为一个向量。然而,如果我们想利用图像是一个 2D 矩阵的事实,我们应该怎么做?一种方法是使用专门的 2D 单元。在本节中,我将简要介绍它们。然而,如果你想了解更多关于它们的内容,我建议你参考这个教程

  • Conv2D:使用卷积核对输入应用卷积以产生输出(称为滤波器)。在训练过程中,这些卷积核会被更新(训练)。确实,它们在密集层中扮演着权重的角色。在 Keras 中,它定义为:
keras.layers.Conv2D(
    filters,
    kernel_size,
    **kwargs
)

其中:filters是卷积核的数量,由于每个卷积核生成一个输出,因此它也代表输出空间的维度。kernel_size是卷积核的大小,通常是一个奇数。

  • MaxPooling2D:在输入窗口上取最大值。在 Keras 中,它定义如下,其中pool_size是用来取最大值的窗口大小:
keras.layers.MaxPooling2D(
    pool_size=(2, 2), **kwargs
)

现在,让我们创建我们的 CNN 模型:

# Create model:
model = Sequential()
model.add(Input(shape=(28, 28, 1)))
model.add(Conv2D(32, kernel_size=3, activation="relu"))
model.add(MaxPooling2D(pool_size=2))
model.add(Conv2D(64, kernel_size=3, activation="relu"))
model.add(MaxPooling2D(pool_size=2))
model.add(Flatten())
model.add(Dropout(0.5))
model.add(Dense(n_labels, activation="softmax"))
print(model.summary())

模型摘要:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
flatten (Flatten)            (None, 1600)              0         
_________________________________________________________________
dropout (Dropout)            (None, 1600)              0         
_________________________________________________________________
dense (Dense)                (None, 10)                16010     
=================================================================
Total params: 34,826
Trainable params: 34,826
Non-trainable params: 0
_________________________________________________________________

该模型是最小的,仅有 34,826 个参数,大约是之前模型的 7 倍小。此外,你会发现它也能实现最高的性能。

5. 训练

正如之前的教程所解释的,训练神经网络就是更新权重,以便模型能够很好地拟合数据。在开始训练之前,需要定义一组参数,包括:优化器、损失函数、批次大小、训练周期数和其他训练过程中需要跟踪的指标。损失函数和附加指标的选择很大程度上取决于输出的类型,无论是回归问题还是分类问题等。

在接下来的所有训练中,我们将优化器设置为'adam',这是一个不错的选择,如果我们不想自己处理学习率的话,批次大小设置为128

5.1. 单一单元输出

由于此模型具有单个输出层,因此我们在之前的教程中介绍和使用的指标和损失函数也可以在这里使用。我们将设置损失函数为mae,附加指标为mse,训练周期数为 200:

# Train:
loss = 'mae'
metric = 'mse'
epochs = 200
model.compile(loss=loss, optimizer='adam', metrics=[metric])
history = model.fit(x_train, y_train, epochs=epochs, batch_size=128, verbose=1, validation_data=(x_val, y_val))

通过观察详细显示,我们可以得出结论:模型已经很好地学习了,并且也能够泛化(训练集和验证集上都取得了良好的结果):

Epoch 200/200
329/329 [==============================] - 1s 2ms/step - loss: 0.0188 - mse: 0.0525 - val_loss: 0.0946 - val_mse: 0.3838

然而,通过使用较少的训练轮次并进行一热编码(第二个模型),我们可以获得更好的结果。

5.2. 一热编码输出

与之前的模型不同,我们使用'categorical_crossentropy'作为损失函数,并将accuracy作为附加指标。'categorical_crossentropy'是一个概率损失函数,用于计算标签和预测之间的交叉熵损失,当标签类别有两个或更多时使用。它期望标签以one_hot表示。至于accuracy,它是一个更适合分类问题的指标。

# Train:
loss = 'categorical_crossentropy'
metric = 'accuracy'
epochs = 20
model.compile(loss=loss, optimizer='adam', metrics=[metric])
history = model.fit(x_train, y_train, epochs=epochs, batch_size=128, verbose=1, validation_data=(x_val, y_val))

通过观察详细显示,我们可以得出结论:模型在仅 20 轮训练中就已经很好地学习了,并且也能够泛化(训练集和验证集上都取得了良好的结果):

Epoch 20/20
329/329 [==============================] - 1s 2ms/step - loss: 0.0469 - accuracy: 0.9838 - val_loss: 0.0868 - val_accuracy: 0.9769

5.3. 卷积单元

现在让我们训练我们的最后一个模型!我们使用与之前模型相同的损失函数和指标,但轮次更少:

# Train:
loss = 'categorical_crossentropy'
metric = 'accuracy'
epochs = 15
model.compile(loss=loss, optimizer='adam', metrics=[metric])
history = model.fit(x_train, y_train, epochs=epochs, batch_size=128, verbose=1, validation_data=(x_val, y_val))

通过观察详细显示,我们可以说模型学习得很好,并且在仅 15 轮训练中成功地泛化(训练集和验证集上的结果更好):

Epoch 15/15
329/329 [==============================] - 4s 14ms/step - loss: 0.0340 - accuracy: 0.9892 - val_loss: 0.0371 - val_accuracy: 0.9893

6. 验证

最后几轮的显示指标不足以得出模型是否从数据中学习的结论。首先需要观察学习曲线。然后,我们可以使用其他指标来评估我们的模型。如果有可用的测试集,我们还会对其进行测试。

不过在这之前,让我们看看如何进行预测并获取预测的类别。

6.1 进行预测

一旦模型训练完成,我们可以开始进行预测:预测输入图像的类别。在这一部分,我们将看到如何获取两种输出类型的预测类别。

在 Keras 中,使用predict函数,其中 x 是输入:

Model.predict(x)

由于模型是基于批次进行训练的,因此在预测之前需要扩展输入维度。我们以训练集的第一个实例为例:

x = np.expand_dims(x_train[0], 0)

现在,我们可以进行预测:

y = model.predict(x)[0]
  • 单一输出预测。 在这种情况下返回的值是一个浮点数,如果模型对给定输入表现不好,该值可能超出范围[0, 9]。第一步是对输出进行裁剪,使所有值都在该范围内,然后将其四舍五入为int,以得到如下输入输出:
print(y)
y = np.clip(y, 0, 9)
print(y)
y = np.rint(y)
print(y)

预测值为 6.9955063,在这种情况下,clip 函数返回相同的值。最后,将该值四舍五入为int:输入类别为 7。

[6.9955063]
[6.9955063]
[7.]
  • 一热编码输出。 在这种情况下,predict 函数返回一个包含 10 个概率元素的列表,因此我们需要获取具有最高概率的元素的索引:
print(y)
y = np.argmax(y)
print(y)

在这里,最高概率是第 8 个元素,对应于类别 7:

[4.8351421e-07 2.1228843e-04 2.9102326e-04 2.4277648e-04 9.7677308e-05
 6.5721008e-07 9.9738841e-08 9.9599850e-01 3.5152045e-06 3.1529362e-03]
7

我向你展示如何进行预测的原因不仅仅是为了了解如何进行预测,还为了在计算一些验证指标时能够使用它。

6.2. 学习曲线

学习曲线揭示了模型在训练期间对已见数据(训练集)和未见数据(验证集)的性能。它允许:

  • 识别过拟合: 当训练损失下降而验证损失增加时。

过拟合示例。

  • 识别欠拟合: 当训练和验证损失都很高时,训练和验证错误都很高。

欠拟合示例:损失函数约为 2,在我们的案例中这是一个较高的值。

  • 通过比较不同模型的学习曲线来比较模型。

训练模型的学习曲线(第一行表示损失函数,第二行表示准确率指标)。我们可以说这三种模型都学得很好,而第三种模型优于其他模型,因为它能够对训练集和验证集中的类进行高准确度预测。换句话说,它的泛化能力更强。

学习曲线可以如下绘制:

# Display loss:
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Single output model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'validation'])
plt.show()
# Display metric:
plt.plot(history.history[metric])
plt.plot(history.history[f'val_{metric}'])
plt.title(f'Single output model {metric}')
plt.ylabel(metric)
plt.xlabel('epoch')
plt.legend(['train', 'validation'])
plt.show()

6.3. 测试集评估

让我们在测试集上评估我们的模型:

# Evaluation:
test_results = model.evaluate(x_test, y_test, verbose=1)
print(f'Test set: - loss: {test_results[0]} - {metric}: {test_results[1]}')

第一种模型:

Test set: - loss: 0.09854138642549515 - mse: 0.4069458544254303

第二种模型:

Test set: - loss: 0.08896738290786743 - accuracy: 0.9793999791145325

第三种模型:

Test set: - loss: 0.02704194374382496 - accuracy: 0.9908000230789185

6.4. 评估指标

训练后,使用额外的指标评估模型。计算的指标很大程度上取决于模型输出的性质:如果是回归(或连续变量),可以使用均方误差(MSE)或平均绝对误差(MAE);如果是分类(或离散变量),可以使用精确度、召回率、F-measure、混淆矩阵和 ROC 曲线。

  • 准确率。 准确率是正确预测与所有预测的比例。

  • 精确度。 精确度表示属于正类的正类预测。它提供了所有预测正确的正类中实际为正的样本数量。

  • 召回率。召回率表示从数据集中正类示例中得到的正类预测。它提供了从所有正类中正确预测的样本数量。

  • F-measure。它是一个平衡精确度和召回率的单一评分。

让我们展示这些指标在训练、验证和测试集上的表现。请注意,以下说明是针对第二种和第三种模型编写的。如果你想展示第一种模型的指标,请参阅第 6.1 节(做出预测),或者访问我的 GitHub 仓库

# Classificati evaluation with output one-hot encoding:
pred_train = np.argmax(model.predict(x_train), axis=1)
pred_val = np.argmax(model.predict(x_val), axis=1)
pred_test = np.argmax(model.predict(x_test), axis=1)
yy_train = np.argmax(y_train, axis=1)
yy_val = np.argmax(y_val, axis=1)
yy_test = np.argmax(y_test, axis=1)
print("Displaying other metrics:")
print("\t\tAccuracy (%)\tPrecision (%)\tRecall (%)")
print(
    f"Train:\t{round(accuracy_score(yy_train, pred_train, normalize=True) * 100, 2)}\t\t\t"
    f"{round(precision_score(yy_train, pred_train, average='macro') * 100, 2)}\t\t\t"
    f"{round(recall_score(yy_train, pred_train, average='macro') * 100, 2)}")
print(
    f"Val :\t{round(accuracy_score(yy_val, pred_val, normalize=True) * 100, 2)}\t\t\t"
    f"{round(precision_score(yy_val, pred_val, average='macro') * 100, 2)}\t\t\t"
    f"{round(recall_score(yy_val, pred_val, average='macro') * 100, 2)}")
print(
    f"Test:\t{round(accuracy_score(yy_test, pred_test, normalize=True) * 100, 2)}\t\t\t"
    f"{round(precision_score(yy_test, pred_test, average='macro') * 100, 2)}\t\t\t"
    f"{round(recall_score(yy_test, pred_test, average='macro') * 100, 2)}")

第一种模型:

Displaying other metrics:
  Accuracy (%) Precision (%) Recall (%) F-measure (%)
Train: 99.67   99.67   99.67   99.67
Val : 97.02   96.99   97.0   96.99
Test: 97.14   97.1   97.11   97.11

第二种模型:

Displaying other metrics:
  Accuracy (%) Precision (%) Recall (%) F-measure (%)
Train: 99.86   99.86   99.86   99.86
Val : 97.98   97.98   97.95   97.96
Test: 97.94   97.94   97.91   97.92

第三种模型:

Displaying other metrics:
    Accuracy (%) Precision (%) Recall (%) F-measure (%)
Train: 99.55   99.56   99.54   99.55
Val : 98.93   98.93   98.92   98.92
Test: 99.08   99.09   99.07   99.08

正如你所见,第三种模型,即基于 CNN 的模型,在测试集和验证集上的表现更好。

  • 混淆矩阵。 混淆矩阵总结了每个类别的所有正确预测和类别之间的所有混淆情况。它提供了模型表现的详细见解,以及它所犯的错误类型。例如,借助混淆矩阵,我们可以了解给定类别与之混淆的其他类别及其程度。此外,如果两个类别之间的混淆程度较高,我们可以理解模型难以区分它们。
# Confusion matrix:
ConfusionMatrixDisplay.from_predictions(yy_val, pred_val, normalize='true')
plt.savefig('output/conv/confmat.png', bbox_inches='tight')
plt.show()

第三模型(基于 CNN 的模型)的混淆矩阵。

6.5. 显示一些数据

显示模型输出总是很有好处。在分类问题中,通常会显示误分类的样本,以获得对模型错误类型的见解。所以,让我们使用 Matplotlib 库显示 10 张误分类的图像:

# create an array of the misclassified indexes
misclass_indexes = np.where(yy_test != pred_test)[0]
# display the 5 worst classifications
fig, axs = plt.subplots(2, 5, figsize=(12, 6))
axs = axs.flat
for i in range(10):
    if i < len(misclass_indexes):
        axs[i].imshow(x_test[misclass_indexes[i]], cmap='gray')
        axs[i].set_title("True: {}\nPred: {}".format(yy_test[misclass_indexes[i]],
                                                     pred_test[misclass_indexes[i]]))
        axs[i].axis('off')
# plt.show()
plt.show()

第三模型的部分误分类数据。

7. 结论

这就是本文的全部内容!在这篇文章中,我们学习了如何创建神经网络,并对分类问题进行训练和验证。本文是“神经网络简要介绍”系列的第二篇教程;其他类型的神经网络将以相同的方式呈现。如果你想深入了解,可以尝试探索和构建其他分类问题的模型(例如 Iris 数据集),同时遵循我在教程中描述的相同流程。尽管本教程旨在介绍神经网络中的分类,但它将作为未来更高级教程的参考。

谢谢,我希望你喜欢阅读这篇文章。你可以在我的GitHub 仓库中找到示例。如果你有任何问题或建议,请随时在下方留言。

[## 神经网络的简要介绍:回归问题

Python 中神经网络的实用初学者指南

关于神经网络的简要介绍:回归问题

参考文献

[1] keras.io/api/layers/activations/#softmax-function

[2] keras.io/api/layers/regularization_layers/dropout/

图片来源

本文中所有未在图注中提及来源的图像和图表均由作者提供。

SciKit 管道简要介绍

原文:towardsdatascience.com/a-brief-introduction-to-scikit-pipelines-888edc86da9b

以及你为什么应该开始使用它们。

Jonte DanckerTowards Data Science Jonte Dancker

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

--

图片由Sigmund提供,来源于Unsplash

你是否曾经训练过机器学习模型,结果看起来太过理想?但后来你发现你的训练数据和测试数据之间存在一些数据泄露?

或者你是否有许多预处理步骤来准备数据,以至于难以将预处理步骤从模型训练迁移到生产中以进行实际预测?

或者你的预处理变得混乱,难以以可读和易于理解的方式分享你的代码?

然后你可能想尝试scikit-learn的管道。管道是设置你的机器学习训练、测试和生产工作流的优雅解决方案,使你的工作更加轻松,结果更具可重复性。

那么什么是管道?它有哪些好处?如何设置管道?我将探讨这些问题,并给出一些构建模块的代码示例。通过结合这些构建模块,你可以构建更复杂的管道,这些管道是针对你需求量身定制的。

什么是管道?

管道允许你在机器学习工作流中组装多个步骤,这些步骤按顺序转换数据,然后将数据传递给估算器。因此,管道可以包括预处理、特征工程和特征选择步骤,然后将数据传递给最终的估算器进行分类或回归任务。

我为什么要使用管道?

总的来说,使用管道会使你的工作更轻松,并加快机器学习模型的开发。这是因为管道

  • 导致更干净、更易于理解的代码

  • 易于复制和理解数据工作流

  • 更易于阅读和调整

  • 使数据准备更快,因为管道自动化了数据准备

  • 有助于避免数据泄露

  • 允许对管道中的所有估计器和参数进行一次超参数优化

  • 方便的是,你只需调用一次fit()predict()就能运行整个数据管道

在训练和优化模型并对结果满意后,你可以轻松保存训练好的管道。然后,每当你想运行模型时,只需加载预训练的管道,你就可以开始推理。这样,你可以以非常干净的方式分享你的模型,易于复制和理解。

如何设置管道?

设置scikit-learn的管道非常简单直接。

scikit-learnPipeline使用包含你要应用于数据的变换器的键值对列表。你可以任意选择键。键可以用来访问变换器的参数,例如,在超参数优化过程中运行网格搜索时。由于变换器存储在列表中,你也可以通过索引访问变换器。

要在管道中拟合数据并进行预测,你可以像使用任何scikit-learn中的变换器或回归器一样运行fit()predict()

一个非常简单的管道可能如下所示:

from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LinearRegression

pipeline = Pipeline(
   steps=[("imputer", SimpleImputer()), 
          ("scaler", MinMaxScaler()), 
          ("regression", LinearRegression())
   ]
)

pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)

不过,scikit-learn还让你的生活更轻松,如果你不想为变换器输入键值,可以使用make_pipeline()函数,scikit-learn会根据变换器的类名设置名称。

from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LinearRegression

pipeline = make_pipeline(steps=[
    SimpleImputer(), 
    MinMaxScaler(), 
    LinearRegression()
    ]
)

就这样。这样你就快速设置了一个简单的管道,可以开始用它来训练模型和运行预测。如果你想查看管道的样子,可以直接打印管道,scikit-learn会向你展示管道的交互视图。

但如果你想构建更复杂和可自定义的东西怎么办?例如,不同地处理分类值和数值,添加特征或转换目标值。

不用担心,scikit-learn提供了额外的功能,可以创建更自定义的管道,并将管道提升到更高的水平。这些功能包括:

  • ColumnTransformer

  • FeatureUnion

  • TransformedTargetRegressor

我将逐一讲解它们,并展示如何使用的示例。

转换选定的特征

如果你有不同类型的特征,例如,连续和分类特征,你可能会希望对这些特征进行不同的转换。例如,对连续特征进行缩放,而对分类特征进行独热编码。

你可以在将特征传递到管道之前进行这些预处理步骤。但这样做的话,你无法将这些预处理步骤和参数包含在以后的超参数搜索中。而且,将它们包含在管道中可以使处理你的机器学习模型更为简便。

要对选择的列应用变换或一系列变换,可以使用ColumnTransformer。其使用方式与Pipeline非常相似,只不过我们将相同的键值对传递给transformers而不是steps。然后,我们可以将创建的变换器作为管道中的一个步骤包含进来。

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder

categorical_transformer = ColumnTransformer(
    transformers=[("encode", OneHotEncoder())]
)

pipeline = Pipeline(steps=[
    ("categorical", categorical_transformer, ["col_name"])
    ]
)

由于我们只想对某些列进行变换,我们需要在管道中传递这些列。此外,我们还可以让ColumnTransformer知道我们希望对其余列做什么。例如,如果你想保留那些未被变换器更改的列,你需要将remainder设置为passthrough。否则,这些列将被丢弃。除了什么都不做或丢弃列,你还可以通过传递一个变换器来变换剩余的列。

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder

categorical_transformer = ColumnTransformer(
 transformers=[("encode", OneHotEncoder(), ["col_name"])], remainder="passthrough"
)

categorical_transformer = ColumnTransformer(
 transformers=[("encode", OneHotEncoder(), ["col_name"])], remainder=MinMaxScaler()
)

Since `scikit-learn` allows Pipeline stacking we could even pass a Pipeline to the `ColumnTransformer` instead of stating each transformation we want to do in the `ColumnTransformer` itself.

from sklearn.compose import ColumnTransformer

from sklearn.impute import SimpleImputer

from sklearn.pipeline import Pipeline

from sklearn.preprocessing import MinMaxScaler, OneHotEncoder

categorical_transformer = Pipeline(steps=[("encode", OneHotEncoder())])

numerical_transformer = Pipeline(

steps=[("imputation", SimpleImputer()), ("scaling", MinMaxScaler())]

)

preprocessor = ColumnTransformer(

transfomers=[

("numeric", numerical_transformer),

("categoric", categorical_transformer, ["col_name"]),

]

)

pipeline = Pipeline(steps=["preprocesssing", preprocessor])


# Combining features

Now, you are able to run different pre-processing steps on different columns, but what if you want to derive new features from the data and add them to you feature set?

For this, you can use `FeatureUnion`, which combines transformer objects into a new transformer with the combined objects. Running a pipeline with a `FeatureUnion` fits each transformer independently and then joins their output.

For example, assume we want to add the Moving Average as a feature, we could do this:

from sklearn.compose import FeatureUnion

from sklearn.pipeline import Pipeline

preprocessor = (

FeatureUnion(

[

("moving_Average", MovingAverage(window=30)),

("numerical", numerical_pipeline),

]

),

)

pipeline = Pipeline(steps=["preprocesssing", preprocessor])


# Transforming the target value

If you have a regression problem sometimes it can help to transform the target before fitting a regression.

You can include such a transformation using the `TransformedTargetRegressor` class. With this class you can either use transformers provided by `scikit-learn` like a MinMax scaler or write your own transformation functions.

One huge advantage of the `TransformedTargetRegressor` is that it automatically maps the predictions back to the original space by an inverse transform. So, you do not need to care about this later on when you move from model training to making predictions in production.

from sklearn.compose import TransformedTargetRegressor

from sklearn.impute import SimpleImputer

from sklearn.pipeline import Pipeline

from sklearn.preprocessing import MinMaxScaler

regressor = TransformedTargetRegressor(

regressor=model,

func=np.log1p,

inverse_func=np.expm1

)

pipeline = Pipeline(

steps=[

("imputer", SimpleImputer()),

("scaler", MinMaxScaler()),

("regressor", regressor)

]

)

pipeline.fit(X_train, y_train)

y_pred = pipeline.predict(X_test)


# Building your own custom functions

Sometimes it is not enough to use pre-processing methods `scikit-learn` provides. This, however, should not hold you back when using Pipelines. You can easily create your own functions that you can then include in the pipeline.

For this, you need to build a class that contains a `fit()` and `transform()` method as these are called when running the pipeline. However, these methods do no necessarily need to do anything. Moreover, we can let the class inherit from `scikit-learn`’s `BaseEstimator` and `TransformerMixin` class to give us some basic functionality that our pipeline needs.

For example, assume we want to make predictions on a time series and we want to smooth all features by a moving average. For this, we just set up a class with a `transform` method that contains the smoothing part.

from sklearn.base import BaseEstimator, TransformerMixin

from sklearn.impute import SimpleImputer

from sklearn.pipeline import Pipeline

from sklearn.preprocessing import MinMaxScaler

class MovingAverage(BaseEstimator, TransformerMixin):

def __init__(self, window=30):

    self.window = window

def fit(self, X, y=None):

    return self

def transform(self, X, y=None):

    return X.rolling(window=self.window, min_periods=1, center=False).mean()

pipeline = Pipeline(

steps=[

("ma", MovingAverage(window=30)),

("imputer", SimpleImputer()),

("scaler", MinMaxScaler()),

("regressor", model),

]

)

pipeline.fit(X_train, y_train)

y_pred = pipeline.predict(X_test)


# What else is there to know?

The default return of transformers in `scikit-learn` is a numpy array. This can lead to problems in your pipeline if you only want to apply a transformation on certain features in the second step of the pipeline, e.g., only categorical features.

However, to prevent your pipeline from breaking you can change the default return value of all transformers to a dataframe by stating

from sklearn import set_config

set_config(transform_output = "pandas")


在进行超参数优化或检查 Pipeline 的单个参数时,直接访问参数可能会很有帮助。要访问参数,你可以使用`<estimator>__<parameter>`语法。例如,在上述示例中,我们可以通过调用`pipeline.set_params(pipeline__ma_window=7)`来访问 MovingAverage 转换器的窗口宽度。

# 结论

使用`scikit-learn`的 Pipeline 可以大大简化你在开发新机器学习模型和设置预处理步骤时的工作。除了拥有许多好处之外,设置 Pipeline 也很简单直接。不过,你可以构建复杂且可定制的预处理 Pipeline,其中只有你的创造力限制了边界。

如果你喜欢这篇文章或有任何问题,随时留言或联系我。我也对你使用`scikit-learn`的 Pipeline 的经验感兴趣。

想要了解更多关于 Pipeline 的信息,请查看以下链接:

+   [`scikit-learn.org/stable/modules/compose.html#pipeline`](https://scikit-learn.org/stable/modules/compose.html#pipeline)
posted @ 2024-10-12 19:51  绝不原创的飞龙  阅读(394)  评论(0)    收藏  举报