【sklearn】模型融合_堆叠法

news2024/11/18 20:00:49

Stacking

  • 参数含义
  • 1. 工具库 & 数据
  • 2. 定义交叉验证函数
    • 2.1 对融合模型
    • 2.2 对单个评估器
  • 3. 定义个体学习器和元学习器
    • 3.1 个体学习器
    • 3.2 元学习器
  • 4. 评估调整模型
  • 5. 元学习器的特征矩阵
    • 5.1 特征矩阵两个问题 & Stacking
    • 5.2 StackingClassfier\Regressor参数cv - 解决样本量少
    • 5.3 StackingClassfier参数stack_method - 解决特征少
    • 5.3 StackingRegressor参数passthrough - 解决特征少
  • 6. Blending
    • 6.1 改进Stacking
    • 6.2 代码实现

参数含义

'''
class sklearn.ensemble.StackingClassfier(estimators, final_estimator=None, *, stack_method='auto', cv=None, n_jobs=None, passthrough=False, verbose=0)
class sklearn.ensemble.StackingRegressor(estimators, final_estimator=None, *, cv=None, n_jobs=None, passthrough=False, verbose=0)

estimators 个体评估器列表
final_estimator 元学习器,只能一个. 分类任务必须为分类算法,回归任务必须为回归算法.
cv 指定交叉验证具体类型、折数, 或简单K折交叉验证
stack_method StackingClassfier独有参数,个体学习器输出的具体测试结果
passthrough 训练元学习器时,是否加入原始数据作为特征矩阵
n_jobs,verbose 线程数与监控参数 
'''

在这里插入图片描述

1. 工具库 & 数据

# 常用工具库
import re
import numpy as np
import pandas as pd
import matplotlib as mlp
import matplotlib.pyplot as plt
import time

# 算法辅助 & 数据
import sklearn
from sklearn.model_selection import KFold, cross_validate
from sklearn.model_selection import train_test_split

from sklearn.datasets import load_digits # 分类-手写数字数据集
from sklearn.datasets import load_iris
from sklearn.datasets import load_boston

# 算法(单一学习器)
from sklearn.neighbors import KNeighborsClassifier as KNNC
from sklearn.neighbors import KNeighborsRegressor as KNNR
from sklearn.tree import DecisionTreeClassifier as DTC
from sklearn.tree import DecisionTreeRegressor as DTR
from sklearn.linear_model import LinearRegression as LR
from sklearn.linear_model import LogisticRegression as LogiR
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.ensemble import GradientBoostingClassifier as GBC
from sklearn.ensemble import GradientBoostingRegressor as GBR
from sklearn.naive_bayes import GaussianNB
import xgboost as xgb

# 融合模型
from sklearn.ensemble import StackingClassifier
from sklearn.ensemble import StackingRegressor
data = load_digits()
X = data.data
y = data.target

print('X.shape =', X.shape)
print('类别:', np.unique(y)) # 10分类

'''
    X.shape = (1797, 64)
    类别: [0 1 2 3 4 5 6 7 8 9]
''' 
Xtrain, Xtest, Ytrain, Ytest = train_test_split(X, y, test_size=0.2, random_state=1107)
print(Xtrain.shape, Xtest.shape, Ytrain.shape, Ytest.shape)
'''
    (1437, 64) (360, 64) (1437,) (360,)
'''

2. 定义交叉验证函数

2.1 对融合模型

def fusion_estimators(clf):
    '''
    对融合模型做交叉验证,对融合模型的表现进行评估
    
    模型融合很容易过拟合,需要保证交叉验证和测试集分数都漂亮.
    '''
    cv = KFold(n_splits=5, shuffle=True, random_state=1107)
    results = cross_validate(clf
                            ,Xtrain, Ytrain
                            ,cv=cv
                            ,scoring='accuracy'
                            ,n_jobs=-1
                            ,return_train_score=True
                            ,verbose=False)
    test = clf.fit(Xtrain, Ytrain).score(Xtest, Ytest)
    print('train_score:{}'.format(results['train_score'].mean())
         ,'\n cv_mean:{}'.format(results['test_score'].mean())
         ,'\n test_score:{}'.format(test)
         )

2.2 对单个评估器

def individual_estimators(estimators):
    '''
    对融合模型中的每个评估器做交叉验证,对单一评估器的表现进行评估
    '''
    for estimator in estimators:
        cv = KFold(n_splits=5, shuffle=True, random_state=1107)
        results = cross_validate(estimator[1]
                                ,Xtrain, Ytrain
                                ,cv=cv
                                ,scoring='accuracy'
                                ,n_jobs=-1
                                ,return_train_score=True
                                ,verbose=False
                                )
        test = estimator[1].fit(Xtrain, Ytrain).score(Xtest, Ytest)
        print('train_score:{}'.format(results['train_score'].mean())
             ,'\n cv_mean:{}'.format(results['test_score'].mean())
             ,'\n test_score:{}'.format(test)
             )

3. 定义个体学习器和元学习器

3.1 个体学习器

'''
沿用Voting投票法(模型融合_投票法)选出的七个模型
'''
# 逻辑回归没有增加多样性选项
clf1 = LogiR(max_iter=3000, C=0.1, random_state=1107, n_jobs=8)
# 增加特征多样性和样本多样性
clf2 = RFC(n_estimators=100, max_features='sqrt', max_samples=0.9, random_state=1107, n_jobs=8)
# 增加特征多样性, 稍微上调特征数量
clf3 = GBC(n_estimators=100, max_features=16, random_state=1107)
# 增加算法多样性,新增决策树与KNN
clf4 = DTC(max_depth=8, random_state=1107)
clf5 = KNNC(n_neighbors=8, n_jobs=8)
clf6 = GaussianNB()

# 新增随即多样性,相同的算法更换随机数种子
clf7 = RFC(n_estimators=100, max_features='sqrt', max_samples=0.9, random_state=1998, n_jobs=-1)
clf8 = GBC(n_estimators=100, max_features=16, random_state=1998)

estimators = [('Logistic Regression', clf1)
             ,('RandomForest', clf2)
             ,('GBDT', clf3)
             ,('Decision Tree', clf4)
             ,('KNN', clf5)
             # ,('Bayes', clf6) # 拖后腿不要了
             ,('RandomForest2', clf7)
             ,('GBDT2', clf8)
             ]

3.2 元学习器

'''
在此选择单个评估器中分数最高的随机森林作为元学习器

分类任务可以选择稍复杂的算法
回归任务需要选择简单的算法
'''
final_estimator = RFC(n_estimators=100
                     # 如果此分裂导致杂质减少大于或等于该值,则该节点将被分裂
                     # ,min_impurity_decrease=0.0025
                     ,random_state=1107
                     ,n_jobs=8
                     )
clf = StackingClassifier(estimators=estimators # Level0的个体学习器
                        ,final_estimator=final_estimator # Level1的元学习器
                        ,n_jobs=8
                        )

4. 评估调整模型

'''
没有过拟合限制,不如投票法
相对于投票法,交叉验证分数降低,测试集分数持平
'''
fusion_estimators(clf)

'''
    train_score:1.0 
     cv_mean:0.9825929152148664 
     test_score:0.9833333333333333
'''
'''
精调过拟合
min_impurity_decrease从0.1往后试
'''
fusion_estimators(clf)
'''
    train_score:1.0 
     cv_mean:0.9825929152148664 
     test_score:0.9833333333333333
'''     

5. 元学习器的特征矩阵

5.1 特征矩阵两个问题 & Stacking

个体学习器在原始数据上训练预测,再把预测结果排布成新特征矩阵,供元学习器学习。

两个问题

  1. 元学习器特征矩阵的特征一定很少

个体学习器数量不多

  1. 元学习器特征矩阵的样本量不多

训练Stacking模型时,分为训练集、验证集和测试集。
测试集训练过程中不能用;
训练集用于训练个体学习器,已透露给个体学习器,再在上面预测则偏高;
只能用验证集,一般只占数据集的30%~40%。

个体学习器预测结果,即元学习器需要训练的矩阵如下排布:
在这里插入图片描述

Stacking堆叠法解决样本量少:

  • 参数 cv

  • 五折交叉验证训练了五个不同参数的模型,每个模型取五份数据中的一份作为预测集,将五次预测结果堆叠即得到一个算法在整个数据集上的预测结果。

  • 如此,任意个体学习器输出的预测值数量 = 样本量;
    特征矩阵的行数 = 原始数据样本量。

5.2 StackingClassfier\Regressor参数cv - 解决样本量少

'''
cv 
    None 默认5折交叉验证
    交叉验证对象 如cv = KFold(n_splits=5, shuffle=True, random_state=1107)
    任意整数 
        表示Stratified K折验证中的折数
        Stratified K折验证考虑标签类别的占比,保证原始标签中类别占比=训练标签中类别占比=验证标签中类别占比
        防止有的标签类别过少导致某一折该类标签不存在
'''
'''
Stacking内部交叉验证不验证泛化能力,只生产数据实现堆叠;
折数很小时,模型容易过拟合;
折数较大时,模型抗过拟合能力会上升,学习能力会略有下降;
数据量足够大时,折数过多不会带来好处,反而降低训练时间.
'''

5.3 StackingClassfier参数stack_method - 解决特征少

'''
特征量即个体学习器数量
一个个体学习器输出的是类别(如0,1,2)只能有一个特征,输出类型若是概率值、置信度等则可拓展多列.
StackingClassfier独有参数stack_method控制个体分类器的输出.
stack_method四种输入字符串:
    'auto' 按后三种顺序尝试,若个体学习器可以使用某一种就选择该种输出
    'predict_proba' 输出概率probability
        对于二分类,输出一列标签为1的概率
        对于n分类,输出n列标签为[0,1,2...,n]的概率
    'decision_function' 输出置信度
        对于二分类,输出一列标签为1的置信度
        对于n分类,输出n列标签为[0,1,2...,n]的置信度
    'predict' 输出预测结果
        输出一列预测标签
    后三种常在sklearn中使用
'''
LR = LogiR(max_iter=3000, random_state=1).fit(Xtrain, Ytrain)
LR.predict_proba(Xtest[:2])

'''
    array([[6.69189045e-15, 9.99986537e-01, 5.91250275e-13, 1.36647983e-11,
            1.33534349e-05, 5.70544031e-13, 1.47472860e-10, 3.56315884e-09,
            1.05798134e-07, 3.58877485e-10],
           [1.98852180e-07, 6.91956491e-07, 1.39758067e-11, 1.16336439e-09,
            3.50962095e-07, 9.99988643e-01, 8.28053059e-06, 2.56757567e-07,
            1.02535554e-06, 5.51767953e-07]])
'''

LR.decision_function(Xtest[:2])
'''
    array([[-12.125248  ,  20.51261851,  -7.64390501,  -4.50356608,
              9.28889507,  -7.67955407,  -2.12474498,   1.06001361,
              4.45089903,  -1.23540807],
           [ -0.91870057,   0.32826077, -10.48168984,  -6.05994616,
             -0.35058408,  14.51199217,   2.81040002,  -0.66312999,
              0.7215324 ,   0.10186528]])
'''

LR.predict(Xtest[:2])
'''
    array([1, 5])
'''

使用stack_method

# 使用stack_method
final_estimator = RFC(n_estimators=100
                     # 如果此分裂导致杂质减少大于或等于该值,则该节点将被分裂
                     # ,min_impurity_decrease=0.0025
                     ,random_state=1107
                     ,n_jobs=8
                     )
clf = StackingClassifier(estimators=estimators # Level0的个体学习器
                        ,final_estimator=final_estimator # Level1的元学习器
                        ,stack_method='auto'
                        ,n_jobs=8
                        )
clf = clf.fit(Xtrain, Ytrain)
'''
transform接口查看元学习器所使用的训练特征矩阵
特征矩阵样本数 = 原数据样本数
7个个体分类器,10分类数据,每个个体分类器输出10个类别对应的概率,共0个特征.
'''
clf.transform(Xtrain).shape , Xtrain.shape

'''
    ((1437, 70), (1437, 64))
'''

5.3 StackingRegressor参数passthrough - 解决特征少

'''
对于回归算法
passthrough
    True 将原始特征矩阵加入个体学习器的预测值,构成新特征矩阵.较高过拟合风险.
    False 不加入
'''

6. Blending

6.1 改进Stacking

数据量大、Stacking过拟合严重时使用Blending

  • Stacking

· 分训练集和测试集.

· 训练集在个体学习器上交叉验证, 验证结果进行堆叠形成元学习器的特征矩阵.

· 隐藏步骤: 之后使用全部训练数据对所有个体学习器训练, 为测试做准备.

  • Blending 学习浅,计算少,不易过拟合

· 分训练集、验证集和测试集.

· 所有个体学习器在训练集上训练, 在验证集上验证, 所有个体学习器验证结果横向拼接形成特征矩阵.

6.2 代码实现

import pandas as pd
from sklearn.model_selection import train_test_split

def BlendingClassifier(X, y, estimators, final_estimator, test_size=0.2, vali_size=0.4):
    '''
    该函数实现Blending分类融合
    X,y: 整体数据集, 会被分割为训练集、验证集和测试集三部分
    estimators: level0的个体学习器, 输入格式形如sklearn中要求的[('名字', 算法),('名字', 算法)...]
    final_estimator: 元学习器
    test_size: 测试集占全数据集的比例
    vali_size: 验证集占全数据集的比例
    '''
    # 分割数据集
    # 1. 分测试集
    # 2. 分训练集和验证集, 验证集占完整数据集的0.4, 因此占排除测试集后的0.4/0.8
    X_, Xtest, y_, Ytest = train_test_split(X,y,test_size=test_size,random_state=1107)
    Xtrain, Xvali, Ytrain, Yvali = train_test_split(X_,y_,test_size=vali_size/(1-test_size),random_state=1107)
    
    # 训练
    # 建立空dataframe用于保存个体学习器上的验证结果, 即用于生成特征矩阵
    # 新建空列表用于保存训练完毕的个体学习器, 以便在测试中使用、
    NewX_vali = pd.DataFrame()
    trained_estimators = []
    # 循环训练每个个体学习器, 并收集个体学习器在验证集上输出的概率
    for clf_id, clf in estimators:
        clf = clf.fit(Xtrain, Ytrain)
        val_predictions = pd.DataFrame(clf.predict_proba(Xvali))
        # 保存结果, 在循环中逐渐构建特征矩阵
        NewX_vali = pd.concat([NewX_vali, val_predictions], axis=1)
        trained_estimators.append((clf_id, clf))
    # 元学习器在特征矩阵上训练, 并输出训练分数
    final_estimator = final_estimator.fit(NewX_vali, Yvali)
    train_score = final_estimator.score(NewX_vali, Yvali)
    
    # 测试
    # 建立空dataframe用于保存个体学习器的预测结果,即用于生成特征矩阵
    NewX_test = pd.DataFrame()
    # 循环, 在每个训练完的个体学习器上进行预测, 并收集每个个体学习器上输出的概率
    for clf_id, clf in trained_estimators:
        test_prediction = pd.DataFrame(clf.predict_proba(Xtest))
        # 保存结果, 在循环中逐渐构建特征矩阵
        NewX_test = pd.concat([NewX_test, test_prediction], axis=1)
    # 元学习器在新特征上测试, 并输出测试分数
    test_score = final_estimator.score(NewX_test, Ytest)
    
    # 打印训练分数与验证分数
    print('train_score=', train_score)
    print('test_score=', test_score)
'''
沿用Voting投票法(模型融合_投票法)选出的七个模型
'''
# 逻辑回归没有增加多样性选项
clf1 = LogiR(max_iter=3000, C=0.1, random_state=1107, n_jobs=8)
# 增加特征多样性和样本多样性
clf2 = RFC(n_estimators=100, max_features='sqrt', max_samples=0.9, random_state=1107, n_jobs=8)
# 增加特征多样性, 稍微上调特征数量
clf3 = GBC(n_estimators=100, max_features=16, random_state=1107)
# 增加算法多样性,新增决策树与KNN
clf4 = DTC(max_depth=8, random_state=1107)
clf5 = KNNC(n_neighbors=8, n_jobs=8)
clf6 = GaussianNB()

# 新增随即多样性,相同的算法更换随机数种子
clf7 = RFC(n_estimators=100, max_features='sqrt', max_samples=0.9, random_state=1998, n_jobs=-1)
clf8 = GBC(n_estimators=100, max_features=16, random_state=1998)

estimators = [('Logistic Regression', clf1)
             ,('RandomForest', clf2)
             ,('GBDT', clf3)
             ,('Decision Tree', clf4)
             ,('KNN', clf5)
             # ,('Bayes', clf6) # 拖后腿不要了
             ,('RandomForest2', clf7)
             ,('GBDT2', clf8)
             ]
final_estimator = RFC(n_estimators=100
                     # ,max_depth=8
                     ,min_impurity_decrease=0.0025
                     ,random_state=1107
                     ,n_jobs=-1
                     )
from sklearn.datasets import load_digits

data = load_digits()
X = data.data
y = data.target
BlendingClassifier(X, y, estimators, final_estimator)

'''
    train_score= 0.9972183588317107
    test_score= 0.9777777777777777
'''    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/163675.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言 动态通讯录实现(附源码)

欢迎来到 Claffic 的博客 💞💞💞 前言: 上期博客写了静态通讯录并且答应给大家写一个动态版,这不,它来了: 1.动态版与静态版的区别 静态版的内存空间开辟大小是固定的,放了预期的最…

Leetcode 剑指 Offer II 010. 和为 k 的子数组

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer(专项突击版)系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 给定一个整数数组和一个整数 k ,请找到该数组中和为…

MTBF是什么意思?交换机做MTBF有什么要求?MTTF、MTBF和MTTR的区别是什么?

MTBF,即平均故障间隔时间,英文全称是“Mean Time Between Failure”。是衡量一个交换机的可靠性指标。单位为“小时”。它反映了交换机的时间质量,是体现交换机在规定时间内保持功能的一种能力。具体来说,是指相邻两次故障之间的平…

【考研】2020-Part A 作文(英一)

可搭配以下链接一起学习: 【考研】2018-Part B 作文(英一)_住在阳光的心里的博客-CSDN博客 目录 一、2020 Part A (一)题目及解析 (二)优秀范文 (三)参考译文 &a…

Ansible playbook 讲解与实战操作

文章目录一、概述二、playbook 核心元素三、playbook 语法(yaml)1)YAML 介绍1、YAML 格式如下2、playbooks yaml配置文件解释3、示例2)variables 变量1、facts:可以直接调用2、用户自定义变量3、通过roles传递变量4、 Host Invent…

LINUX---文件

目录第一部分:文件编程一.打开/创建文件二.文件的写入操作三.文件的读取四.文件的光标应用:计算文件的大小第二部分:文件操作原理:一.文件描述符静态文件和动态文件第三部分:文件编程小应用1.实现CP命令2.修改文件3.写…

安卓玩机搞机技巧综合资源-----修改rom 制作rom 解包rom的一些问题解析【二十一】

接上篇 安卓玩机搞机技巧综合资源------如何提取手机分区 小米机型代码分享等等 【一】 安卓玩机搞机技巧综合资源------开机英文提示解决dm-verity corruption your device is corrupt. 设备内部报错 AB分区等等【二】 安卓玩机搞机技巧综合资源------EROFS分区格式 小米红…

【Vue笔记】Vue组件的创建、使用以及父子组件数据通信常见的几种方式

这篇文章,主要介绍Vue组件的创建、使用以及父子组件数据通信常见的几种方式。 目录 一、Vue组件的使用 1.1、局部组件 1.2、全局组件 1.3、动态组件(组件动态切换) 1.4、缓存组件 (1)如何缓存组件 (…

微服务技术--Nacos与Eureka

eureka注册中心 远程调用的问题 消费者该如何获取服务提供者具体信息? 服务提供者启动时向eureka注册自己的信息eureka保存这些信息消费者根据服务名称向eureka拉取提供者信息 如果有多个服务提供者,消费者该如何选择? 服务消费者利用负载均…

区块链技术1---密码学基础

摘要:BTC属于加密货币,其中必然涉及到密码学的知识,而比特币比较开放,交易记录,交易金额甚至是底层源代码都是对外开放,那么加密使用在何处?这里就来谈一谈1:哈希哈希函数是密码学的…

client-go实战之六:时隔两年,刷新版本继续实战

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 系列文章链接 client-go实战之一:准备工作client-go实战之二:RESTClientclient-go实战之三:Clientsetclient-go实战之四:…

JavaWeb开发(三)3.3——Spring Bean详解

一、Bean的概念 由 Spring IoC 容器负责创建、管理所有的Java对象&#xff0c;这些管理的对象称为 Bean&#xff0c;Bean 根据 Spring 配置文件中的信息创建。 二、基于XML方式管理bean对象 eg&#xff1a; <?xml version"1.0" encoding"UTF-8"?&…

【B-树、B+树、B* 树】多叉平衡搜索树,解决“IO次数”与“树高”问题~

目录 一、为什么会出现B-树&#xff1f; 面试题&#xff1a; 二、什么是B-树&#xff1f; 2.1、B,B-树,B*树 导航 三、B-树的模拟实现 3.1、插入结点分析 3.1.1、根节点的分裂 3.1.2、继续插入数据&#xff0c;分裂子节点 3.2.3、再次插入数据&#xff0c;导致根节点继…

tomcat和apache有什么区别?如何将内网发布到互联网访问?

tomcat、 apache是比较常用的搭建服务器的中间件&#xff0c;它们之间还是有一些区别差异的&#xff0c;我们通常会根据本地应用场景来选择合适的中间件来搭建服务器。在内网本地部署搭建服务器后&#xff0c;还可以通过快解析端口映射方法&#xff0c;将内网应用地址发布到互联…

Android原生检测Selinux的三种方法

本文介绍 3 种检测 Android 设备 SELinux 状态的方法, Java 层检测Selinux已经没有太多意义,因为不是很靠谱,随便一个hook代码就能绕过,所以我要告诉你如何在 C 层完成检测。这几种方法在效率和抵抗mock SELinux State 的技术方面都不相同,因此在使用之前你需要知道每种方…

Windows server——部署DNS服务

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 本章重点 一.DNS概述 1.DNS的诞生 二.DNS的功能 使用域名访问具有以下优点…

【大厂高频真题100题】《二叉树的序列化与反序列化》 真题练习第23题 持续更新~

二叉树的序列化与反序列化 序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。 请设计一个算法来实现二叉树的序列化与反序列化。这里不限…

c语言 图形化贪吃蛇 多种功能 无需安装第三方库 课设 (附代码)

前言 类贪吃蛇是利用c语言模仿并实现经典游戏贪吃蛇&#xff0c;使其在窗口有贪吃蛇活动的规定范围&#xff0c;并完成一系列包括但不限于模仿蛇的移动&#xff0c;方向控制&#xff0c;吃到食物加分&#xff0c;撞上墙壁及蛇头碰到蛇身死亡等游戏功能。 附加功能&#xff1a…

软件测试复习03:动态测试——白盒测试

作者&#xff1a;非妃是公主 专栏&#xff1a;《软件测试》 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录逻辑覆盖法&#xff1a;最常用程序插桩技术基本路径法点覆盖边覆盖边对覆盖主路径覆盖符号测试错误…

前端leaflet框选下载bing遥感图

微软必应bing遥感图。bing地图比百度遥感图清晰很多&#xff0c;19级&#xff0c;百度是18级&#xff0c;同样的18级&#xff0c;bing地图比百度也清晰很多。所以没有必要用百度地图了。不过bing地图仅用于学习&#xff0c;商用要付费。参考了https://xiaozhuanlan.com/topic/6…