credentials =({"account":"3-4",# Combine 3 and 4 with a hyphen"username":"bexgboost",# Your username in lowercase"password":"your_password",# Your Snowflake password},)技术支持
from snowflake.snowpark.types import DoubleType
numeric_features =["CARAT","X","Y","Z","DEPTH","TABLE_"]for col in numeric_features:
diamonds_df = diamonds_df.with_column(col, diamonds_df[col].cast(DoubleType()))list(diamonds_df.select(*numeric_features))技术支持
import snowflake.ml.modeling.preprocessing as snowml
from snowflake.ml.modeling.pipeline import Pipeline技术支持
需要注意的是,Snowpark 的 DataFrame API 位于 snowflake.snowpark 下,而 Snowpark 的 ML 模块位于 snowflake.ml 下。一开始可能有点混淆,请耐心适应。
在下面的代码中,我们将:
列出需要编码的类别特征名。
为输出特征命名,因为 Snowpark 会将编码后的特征作为新列添加,而不是替换旧列。
定义有序类别的正确顺序;在 Diamonds 数据集中,所有类别都具备内在顺序,从左到右价格越高。
# List all the features for processing
cat_cols =["CUT","COLOR","CLARITY"]
cat_cols_encoded =["CUT_OE","COLOR_OE","CLARITY_OE"]# We already have numeric_features# List the correct ordering of categorical features
categories ={"CUT": np.array(["IDEAL","PREMIUM","VERY_GOOD","GOOD","FAIR"]),"CLARITY": np.array(["IF","VVS1","VVS2","VS1","VS2","SI1","SI2","I1","I2","I3"]),"COLOR": np.array(["D","E","F","G","H","I","J"]),}技术支持
import joblib
PIPELINE_FILE ="pipeline.joblib"# Pickle locally first
joblib.dump(preprocessing_pipeline, PIPELINE_FILE)
transformed_diamonds_df = preprocessing_pipeline.fit(clean_df).transform(clean_df)# transformed_diamonds_df.show() - commented because of long output技术支持
该代码片段运行无误,说明我们的管道工作正常,这也意味着我们终于可以训练模型了。
在 Snowpark 中训练 XGBoost 模型
为了训练模型,我们再次列出特征名和目标名:
# Define the columns again
cat_cols_encoded =["CUT_OE","COLOR_OE","CLARITY_OE"]
numeric_features =["CARAT","X","Y","Z","DEPTH","TABLE_"]
label_cols =["PRICE"]# Must be a list
output_cols =["PRICE_PREDICTED"]# Required in snowpark技术支持
# Split the data
train_df, test_df = diamonds_df.random_split(weights=[0.8,0.2], seed=42)技术支持
随后,加载管道并对训练集与测试集进行拟合与变换。
# Load the pre-processing pipeline locally
pipeline = joblib.load("pipeline.joblib")# Apply it to both dataframes
train_df_transformed = pipeline.fit(train_df).transform(train_df)
test_df_transformed = pipeline.transform(test_df)技术支持
需要注意的是,在测试集上只调用 .transform(),以避免数据泄漏。
接着,从 ml.modeling.xgboost 子模块中初始化一个 XGBoost 回归模型:
# Snowpark has models from scikit-learn and lightgbm toofrom snowflake.ml.modeling.xgboost import XGBRegressor
# Initialize
regressor = XGBRegressor(
input_cols=cat_cols_encoded + numeric_features,
label_cols=label_cols,
output_cols=output_cols,)技术支持
# Set up for Registryfrom snowflake.ml.registry import Registry
# Get the current db and schema name
db_name = new_session.get_current_database()
schema_name = new_session.get_current_schema()# Define global model name for the project
model_name ="diamond_prices_regression"# Initialize a registry to log models
registry = Registry(session=new_session, database_name=db_name, schema_name=schema_name)技术支持
我们将使用 Registry.log_model() 方法来保存模型:
# Get sample data to pass into registry for schema
sample = train_df.select(cat_cols_encoded + numeric_features).limit(50)# Log the first model
v0 = registry.log_model(
model_name=model_name,
version_name="v0",
model=regressor,
sample_input_data=sample,)技术支持
# Add the models RMSE score
v0.set_metric(metric_name="RMSE", value=rmse_test)# Add a description
v0.comment ="The first model to predict diamond prices"技术支持
# Log the optimal model
v1 = registry.log_model(
model_name=model_name,
version_name="v1",
model=regressor,
sample_input_data=sample,)# Add the models RMSE score
v1.set_metric(metric_name="RMSE", value=optimal_rmse_test)# Add a description
v1.comment ="Optimal model found with RandomizedSearchCV"技术支持
为确认模型已保存,您可以调用 .show_models():
# Confirm the models are added
registry.show_models()技术支持
# Doing inference with the optimal model
optimal_version = registry.get_model(model_name).version("v1")
results = optimal_version.run(test_df, function_name="predict")
results.columns技术支持
get_model() 返回的是通用的 model 对象,与 XGBRegressor 不同。因此您需要通过指定函数名来调用 .run() 执行推理。
什么是 Snowflake Snowpark?
传统的机器学习流程通常是把数据库中的数据搬到模型所在的位置。随着近年来 AI 的兴起以及数据集规模的激增,这种做法越来越不切实际。
为了完成清洗、分析和模型训练,需要将 TB 级数据从数据库传输到客户端应用。这趟看似无害的“往返旅程”会浪费大量宝贵的资源。基于此,越来越多公司选择使用库内(in-database)技术,尽量减少数据移动并让数据操作更顺畅。
市面上最好的库内技术之一是由 Snowflake Cloud 提供的 Snowpark。Snowpark 是一组库和运行时,允许您在 Snowflake Cloud 中安全地运行编程语言,在与 Snowflake 数据库相同的环境中开发数据管道和机器学习模型。
在本教程中,我们将介绍 Snowpark 的基础知识以及如何在您的项目中使用它。我们假设您已经熟悉 SQL 和 Snowflake——如果您需要先补充这部分内容,可以学习我们的 SQL 基础技能路径,或阅读这篇Snowflake 新手教程。
让我们开始吧!
为什么选择 Snowflake Snowpark?
Snowflake Snowpark 是一组库和运行时,允许您安全地使用 Python、Java、Scala 等编程语言,直接在 Snowflake 云平台内处理数据。
这消除了为处理而将数据移出 Snowflake 的需求,从而提升效率与安全性。其主要优势包括:
简而言之,Snowpark 为开发者提供了一种强大而简洁的方式,直接在 Snowflake Cloud 中构建数据管道、机器学习解决方案与数据驱动应用。
Snowpark 入门
本教程的最终目标是使用 Snowpark,在 Snowflake 数据库的一张表上训练并调参一个模型。为此,我们将先完成以下步骤:
创建虚拟环境
本教程中,我们将使用一个新的
conda环境:另外提醒一下,如果您是在新安装的
conda环境中运行,激活snowpark环境前需要先执行$conda init。激活后,需要安装以下库:
如果您将使用 Jupyter,还需安装
ipykernel,并运行以下命令将当前环境添加为一个 Jupyter 内核:现在导入一些后续会用到的通用库:
将示例数据导入 Snowflake
本教程将使用 Seaborn 的 Diamonds 数据集——您可以直接从我的 GitHub下载 CSV 文件。
登录Snowflake 应用后,按照下方 GIF 的步骤,将该数据集导入一张新的数据库表(若您还没有账号,请先创建新账号):
在实际工作中,您很少会以 CSV 文件的形式将表导入 Snowflake。多数情况下,您会在公司数据库管理员授予的不同访问级别下,直接使用已有数据库。
创建 Snowpark 会话
接下来需要在 Snowpark 的 API 与 Snowflake 云之间建立连接,以查询我们的数据库。该连接需要以下 Snowflake 凭据:账户名、用户名和密码。
下图展示了如何从 Snowflake 控制台获取您的用户名和账户名:
创建一个名为
config.py的独立文件,包含一个名为credentials的字典,格式如下:出于安全考虑,我们将凭据存放在单独的文件中。将该文件加入
.gitignore可以确保您的 Snowflake 凭据不会被意外泄露。现在,导入
credentials字典和 Snowpark 的Session类:为建立连接,我们将使用
Session.builder.configs.create()方法:如果打印出了您的用户名,说明首次会话已成功创建!
将已导入数据加载到会话
new_session对象可以访问您在 Snowflake 账户中拥有(或被授予权限)的所有内容。在导入数据之前,第一步是告知会话我们将使用上一节创建的
test_db数据库:我们通过 .sql 方法完成这一步,它允许我们运行任何与 Snowflake SQL 兼容的查询。
现在,我们可以使用会话对象的 table 方法加载
test_db的diamonds表:.show()方法相当于 Pandas 的DataFrame.describe(),可以让我们确认已成功连接至该表。没错——我们只是连接到了 diamonds 表。从其大小也能看出
diamonds_df对象并未持有任何数据:它只有 48 字节,而实际上应该超过 3 MB。我们再花点时间理解这是为什么。
理解 Snowpark DataFrame
到目前为止,我们已完成以下步骤:
现在让我们来讨论 Snowpark DataFrame!
Snowpark DataFrame 采用惰性计算
Pandas DataFrame 使用的是您的本机内存(RAM),也就是说它们驻留在您的机器上。相较之下,Snowpark DataFrame 虽然可以在编码环境中查看其表示,但其实是驻留在 Snowflake 的云平台上。数据保存在云端,无需下载到本地即可对其进行操作。
另一个重要特性是 Snowpark DataFrame 采用惰性计算。这意味着在您明确要求执行之前,它们不会对数据执行任何操作。
取而代之的是,它们会构建所需操作的逻辑表示,然后将其转换为优化过的 SQL 语句,由 Snowflake 来执行。
相比之下,Pandas 会立即执行操作——也就是“及早执行”。
从性能角度看,惰性计算通常显著快于及早执行。由于惰性 DataFrame 利用的是 Snowflake 强大的弹性计算资源,它们可以将操作下推到数据库层面,从而运行得更快。
为了更好地理解惰性计算与及早执行的差异,可以将我们在 Snowflake 的数据想象成一座巨大的图书馆。我们有以下两种情景:
何时使用 Snowpark DataFrame
既然 Snowpark 可以转换为 Pandas DataFrame,那么我们该在何时选择其一?
答案很大程度取决于数据集的大小。
如果您的数据集很小(比如 Diamonds 数据集),您可以将其下载到本地并交给 Pandas 处理(这正是
DataFrame.to_pandas()方法所做的)。但在今天的数据规模下,事情很容易失控,下载数据库可能要等上几个小时。并且别忘了,这是一张“往返票”——任何想要保留的新数据也必须传回去。这正是学习 Snowpark DataFrame API 的价值所在。
另一种方式是使用
Session.sql()方法,它允许我们执行任何与 Snowflake 兼容的 SQL 表达式(见下例)。不过我强烈建议掌握 Snowpark DataFrame API,因为它内置了 100 多个 SQL 函数与表达式的函数封装。Snowpark DataFrame 的变换函数
您在 Snowpark 中的大部分时间都会花在变换函数上。它们位于以下子模块下:
F下的所有函数(可通过dir(F)列出)都用于指定 DataFrame 应如何变换。来看一个示例:这里,col 函数创建对给定列的引用并返回一个
Column对象。然后,.upper()将 CUT 列的值转换为大写:但如果我们查看 diamonds 数据集,会发现
CUT列中的值仍有小写字母。为什么?因为F.upper(F.col("CUT"))表达式等价于下面的 SQL 查询:这里缺了什么?
当然是指定应用变换的表名的
FROM子句!因此,我们必须将
F.upper(F.col("CUT"))表达式与diamonds_df对象结合起来。主要有三种方式:.select().filter().with_column()由于我们的表达式是列变换,因此传给
.select():我们已成功改变了这些值!
现在来写一个筛选行的表达式:
我们发现数据集中有 5000 多颗钻石的价格超过 1 万美元。再试试创建新列:
上面的例子也表明可以进行方法链式调用——我们创建了一个名为 “PRICE_SQUARED” 的新列。可以看到,Snowpark DataFrame 的函数与 Pandas 十分类似。
总之,如果您在寻找某个 Pandas 函数在 Snowpark 中的替代品,它要么在 F 下,要么是 Snowpark DataFrame 的方法。您可以通过
dir(F)或dir(diamonds_df)查看,或阅读Snowpark Python 参考文档。在 Snowpark DataFrame 上进行 EDA
探索性数据分析(EDA)的重要一环是可视化。幸运的是,我们不需要为此依赖 Snowpark,可以继续使用现代数据栈。
思路是这样的:EDA 主要是发掘总体趋势和洞见,我们常常可以用一个样本来代替整个数据集。下载具有代表性的样本后,将其转换为 Pandas DataFrame,就能使用熟悉的 Matplotlib 或 Seaborn。
先进行转换:
上面我们将 25% 的数据集下载为一个 Pandas DataFrame。接下来把列名转换为小写:
现在,您就可以像对任何数据集那样进行 EDA 了。下面我们探索一些思路,先生成一个散点图来可视化价格与克拉的关系:
接下来绘制相关矩阵,查看数值特征两两之间的相关性:
我们还可以为钻石切工类别绘制柱状图:
为了确认在整个数据集上这些图形大致相同,我们将运行上面柱状图的 SQL 版本。
首先,编写按 CUT 对 diamonds 数据集分组的 SQL 表达式:
现在将结果转换并下载为 Pandas DataFrame:
这一次,我们使用
plt.bar()来绘图:虽然柱子的顺序不同,但并排查看可以发现传达的信息是一致的。
在 Snowpark 中训练机器学习模型
我们已经准备好学习如何在 Snowpark 中训练机器学习模型了!
我们从清洗数据集开始。
在 Snowpark 中清洗数据
首先,确保所有列的数据类型正确:
table列的命名有点古怪,我们把它转换为大写的TABLE_(在末尾加下划线以避免与 SQL 关键字冲突):我们需要将数值特征从
DecimalType改为DoubleType,因为 Snowpark 目前尚不支持 decimals。在上述代码片段中,我们再次使用了
.with_column()方法。另一个新方法是.cast(),它属于 Snowpark DataFrame 的方法。Snowpark 还要求在编码前,所有文本特征必须为大写且词间没有空格。目前
CUT特征的取值不满足这一要求,我们将通过函数变换来修正:现在数据已无清洗方面的问题。我们可以将其作为新表保存回 Snowflake:
务必使用
overwrite模式,因为您可能需要回头修改某些清洗操作并再次保存。在 Snowpark 中为建模做准备
本节我们将处理剩余的预处理问题,避免阻碍模型训练。先加载上一节保存的清洗后表:
很好!
接着,从
snowflake.ml命名空间导入preprocessing和pipeline子模块。需要注意的是,Snowpark 的 DataFrame API 位于
snowflake.snowpark下,而 Snowpark 的 ML 模块位于snowflake.ml下。一开始可能有点混淆,请耐心适应。在下面的代码中,我们将:
现在,我们来构建一个类似 Scikit-learn 的管道:
Pipeline类接受一个步骤列表,每个步骤都是一个预处理类。这里我们使用OrdinalEncoder将类别编码为 0、1、3 等,用StandardScaler规范化数值特征。最后,我们使用
joblib将管道保存在本地,并在整个数据集上测试:该代码片段运行无误,说明我们的管道工作正常,这也意味着我们终于可以训练模型了。
在 Snowpark 中训练 XGBoost 模型
为了训练模型,我们再次列出特征名和目标名:
现在使用
DataFrame.random_split()划分数据,并指定权重来表示训练集与测试集的比例:随后,加载管道并对训练集与测试集进行拟合与变换。
需要注意的是,在测试集上只调用
.transform(),以避免数据泄漏。接着,从
ml.modeling.xgboost子模块中初始化一个 XGBoost 回归模型:XGBRegressor类需要提供所有输入名、所有目标名以及输出列名。提供后,调用.fit()开始训练:如果您使用的是 Snowflake 免费账号,受限于计算资源且无 GPU,训练可能会比较慢。
训练完成后,我们可以生成预测:
# 预测
来看一下预测结果:
我们现在需要评估初始模型的表现。Snowpark 在其
ml.modeling.metrics子模块下包含了来自 Scikit-learn 的数十种指标,我们将其导入为M:然后两次运行
mean_squared_error来计算训练集和测试集的均方根误差(RMSE)(下面squared=False表示返回 RMSE 而非 MSE):我们的模型平均相差 542 美元,并且可能存在一定过拟合,因为训练与测试 RMSE 的差距较大。
让我们通过调参来尝试解决这些问题。
在 Snowpark 中进行超参数调优
目前,Snowpark 提供两种超参数搜索类:
GridSearchCV:通过交叉验证穷举所有超参数组合。RandomizedSearchCV:通过交叉验证在给定超参数分布中进行随机搜索。XGBoost 大约有十来个可提升性能的超参数(您可以在这篇Python 中使用 XGBoost的教程中了解更多)。这促使我们选择随机搜索。就个人而言,我更愿意使用采用贝叶斯搜索的 Optuna,但在 Snowpark 中目前还无法使用。
现在来定义搜索:
我们将基学习器数量固定为 2000,只调优
max_depth和learning_rate。rscv的默认参数n_iter=10表示将执行 10 次搜索,每次随机选择一组参数组合。若要获得更准确的结果,最好将该参数设得更大一些。现在评估最优模型的表现。代码与之前相同,但我们使用
rscv对象来代替regressor:训练 RMSE 下降了,但测试 RMSE 更大了。这说明我们仍在过拟合,需要扩大搜索范围,并加入其他用于剪枝的参数,以约束 XGBoost 底层的决策树。
这部分就留给您来完成了。
在 Snowpark 中保存最佳模型
如果现在关闭会话,我们将丢失已调优的模型。要保存它,我们将使用 Snowpark 的原生模型注册表(model registry),它是一个虚拟存储,用于保存任意模型及其元数据。
注册表通过
Registry类提供,需要传入当前会话的数据库与架构名。同时还需要为项目指定一个名称,以避免与其他项目的模型发生冲突:我们将使用
Registry.log_model()方法来保存模型:我们可以使用
.set_metric()方法为模型记录指标(可以记录任意多个)。我们还会添加一条注释来描述该模型:对于随机搜索得到的最优模型,同样进行记录。记录指标时别忘了将值改为
optimal_rmse_test。为确认模型已保存,您可以调用
.show_models():关于如何管理注册表的更多信息,请参阅 Snowpark 开发者文档此页面。
在 Snowpark 中进行推理
推理通常通过从注册表中选择最佳模型来完成。我们可以通过筛选
.show_models()的结果来实现,或像下面这样直接使用版本标签检索模型:get_model()返回的是通用的model对象,与XGBRegressor不同。因此您需要通过指定函数名来调用.run()执行推理。总结
在本教程中,我们完成了在 Snowflake Snowpark 中训练机器学习模型的端到端流程。做得很好!
我们从一个“骨架版”的未清洗数据集出发,最终得到一个已调参的 XGBoost 回归模型。所有操作均在 Snowflake Cloud 上完成,也就意味着我们几乎无需占用本地系统资源。
我们了解到,在大数据集场景下,这类库内操作的效率非常高——而 Snowflake Snowpark 是推动这一过程的市场上最优秀的工具之一。
如果您想进一步了解 Snowpark 或 Snowflake,请查看以下资源:
感谢您的阅读!