当前位置: 首页 > news >正文

机器学习练手(四):基于SVM 的肥胖风险分类

总结:本文为和鲸python 机器学习原理与实践·闯关训练营资料整理而来,加入了自己的理解(by GPT4o)

原活动链接

原作者:vgbhfive,多年风控引擎研发及金融模型开发经验,现任某公司风控研发工程师,对数据分析、金融模型开发、风控引擎研发具有丰富经验。

在上一关中学习了如何训练决策树模型、可视化决策树、计算特征重要性、计算 r2 分数指标等内容,下面我们将学习今天的内容 SVM 支持向量机。

目录

  • SVM
      • 肥胖风险数据集之二分类问题
        • 引入依赖
        • 加载数据
        • 数据基础分析
        • 特征预处理
        • 训练模型
        • 如何确定核函数?
        • 重新建立模型预测测试集并计算指标
        • 总结
      • 课后思考题
      • 闯关题

vgbhfive,多年风控引擎研发及金融模型开发经验,现任某公司风控研发工程师,对数据分析、金融模型开发、风控引擎研发具有丰富经验。

总结:依然重点关注对于数据的处理及模型核函数的选取,包括:

删除空值(删除前观察空值分布)

查看非数值型数据的分布,使用LabelEncoder()处理数据

查看各维度与预测值的相关性,并删除相关性较小的维度

在SVM模型构建中通过定义不同的核函数并进行交叉验证来评估每种核函数的性能,注意需要对数据集进行标准化,否则定义不同核函数的SVM并进行交叉验证时会很慢,尤其是sigmoid核函数,一晚上都跑不完。进行标准化转换后,几分钟出结果。

SVM

SVM 支持向量机,指定义在特征空间上的间隔最大的线性分类器。单从名字来看该算法是最摸不着头脑的算法,不过在了解其定义之后可以明白其是对逻辑回归的演进,其重点在于扩展特征维度空间

在之前在逻辑回归问题中,会将日常问题抽象为二维特征空间内的问题,那如果将日常问题映射到三维特征空间,或者多维特征空间,会不会更好区分呢?

SVM 的重点就是求解该映射关系的参数,映射关系在 SVM 中被称为核函数

目前可用的核函数有以下:

  • 线性核(Linear Kernel):适用于线性可分的数据。如果你的数据在特征空间中可以通过一条直线分割,线性核可能是一个好选择。它计算速度快,参数少,对于一般数据,分类效果已经很理想。
  • 多项式核函数:适用于非线性问题,可以将特征映射到更高次幂的多项式空间。核函数的阶数由参数 degree 决定,决策边界为多项式的曲面。
  • 径向基函数(RBF)核:又称高斯核,适用于线性不可分的数据。RBF 核可以将数据映射到一个高维空间,使得数据在新的空间中更容易分割。其具有更多的参数,并且分类结果非常依赖于参数的选择,通过训练数据的交叉验证来寻找合适的参数是常见的做法,但这个过程可能比较耗时。
  • Sigmoid 核函数:类似于神经网络的激活函数,适用于需要逻辑回归模型的决策边界,即收拢最终的结果只在一定边界内。

肥胖风险数据集之二分类问题

随着现在生活水平的提高,现在已经不必为了吃饱肚子而是为了更好吃的食物,因此导致的肥胖问题也更加严重。为了更好地理解肥胖的成因,预测个体的肥胖风险,并为预防和治疗提供支持,现计划开发一个基于机器学习的肥胖风险评估模型。该数据集收集了大量人群的健康数据,共有20758 例记录,其中包括年龄、性别、体重、身高、家族肥胖史、身体活动量、饮食习惯、运动方式等特征信息。

肥胖风险数据集数据含义如下:

特征列名称特征含义
Gender性别
Age年龄
Height身高
Weight体重
family_history_with_overweight家族肥胖史
FAVC是否频繁食用高热量食物
FCVC食用蔬菜的频次
NCP食用主餐的次数
CAEC两餐之间的食品消费:always(总是);frequently(经常);sometimes(有时候)
SMOKE是否吸烟
CH2O每日耗水量
SCC高热量饮料消耗量
FAF运动频率
TUE使用电子设备的时间
CALC酒精消耗量:0(无); frequently(经常);sometimes(有时候)
MTRANS日常交通方式:Automobile(汽车);Bike(自行车);Motorbike(摩托车);Public Transportation(公共交通);Walking(步行)
0be1dad肥胖水平
引入依赖
import pandas as pd
import numpy as np
import matplotlib.pyplot as pltfrom sklearn import svm
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score
加载数据
# 1. 加载数据obesity = pd.read_csv('./data/obesity_level-4.csv', index_col='id')
obesity.head()
GenderAgeHeightWeightfamily_history_with_overweightFAVCFCVCNCPCAECSMOKECH2OSCCFAFTUECALCMTRANS0be1dad
id
0Male24.4430111.69999881.669950112.0000002.983297Sometimes02.76357300.0000000.976473SometimesPublic_TransportationOverweight_Level_II
1Female18.0000001.56000057.000000112.0000003.000000Frequently02.00000001.0000001.0000000Automobile0rmal_Weight
2Female18.0000001.71146050.165754111.8805341.411685Sometimes01.91037800.8660451.6735840Public_TransportationInsufficient_Weight
3Female20.9527371.710730131.274851113.0000003.000000Sometimes01.67406101.4678630.780199SometimesPublic_TransportationObesity_Type_III
4Male31.6410811.91418693.798055112.6796641.971472Sometimes01.97984801.9679730.931721SometimesPublic_TransportationOverweight_Level_II
数据基础分析
# 2. 数据基础分析obesity.info()
# 数据项各列不存在空值
<class 'pandas.core.frame.DataFrame'>
Index: 20758 entries, 0 to 20757
Data columns (total 17 columns):#   Column                          Non-Null Count  Dtype  
---  ------                          --------------  -----  0   Gender                          20758 non-null  object 1   Age                             20758 non-null  float642   Height                          20758 non-null  float643   Weight                          20758 non-null  float644   family_history_with_overweight  20758 non-null  int64  5   FAVC                            20758 non-null  int64  6   FCVC                            20758 non-null  float647   NCP                             20758 non-null  float648   CAEC                            20758 non-null  object 9   SMOKE                           20758 non-null  int64  10  CH2O                            20758 non-null  float6411  SCC                             20758 non-null  int64  12  FAF                             20758 non-null  float6413  TUE                             20758 non-null  float6414  CALC                            20758 non-null  object 15  MTRANS                          20758 non-null  object 16  0be1dad                         20758 non-null  object 
dtypes: float64(8), int64(4), object(5)
memory usage: 2.9+ MB
# 3. 结果列的分布情况obesity['0be1dad'].value_counts()
0be1dad
Obesity_Type_III       4046
Obesity_Type_II        3248
0rmal_Weight           3082
Obesity_Type_I         2910
Insufficient_Weight    2523
Overweight_Level_II    2522
Overweight_Level_I     2427
Name: count, dtype: int64
特征预处理
# 4. 对分类型特征值进行编码lb = LabelEncoder()
columns = ['Gender', 'CAEC', 'CALC', 'MTRANS', '0be1dad']
for col in columns:obesity[col] = lb.fit_transform(obesity[col])
# 5. 查看各个特征与结果列的相关性# 选择数值型列
numeric_cols = obesity.select_dtypes(include=[float, int]).columns# 计算相关性矩阵
correlation_matrix = obesity[numeric_cols].corr()
# 提取与 'Price' 列相关的相关性值
price_correlation = correlation_matrix['0be1dad']# 打印结果
print(price_correlation)
# print(cars.corr()['Price'])
Gender                            0.033655
Age                               0.269122
Height                            0.073753
Weight                            0.410058
family_history_with_overweight    0.298750
FAVC                              0.016886
FCVC                              0.054972
NCP                              -0.089360
CAEC                              0.175111
SMOKE                            -0.008864
CH2O                              0.182406
SCC                              -0.051350
FAF                              -0.097437
TUE                              -0.056894
CALC                              0.124926
MTRANS                           -0.081371
0be1dad                           1.000000
Name: 0be1dad, dtype: float64
from sklearn.preprocessing import StandardScaler
# 初始化特征缩放器
scaler = StandardScaler()
# 缩放特征
x_scaled = scaler.fit_transform(obesity[numeric_cols])
# 将缩放后的特征转换为 DataFrame
x_scaled = pd.DataFrame(x_scaled, columns=numeric_cols)
# 6. 拆分特征列和结果列y = obesity['0be1dad']
x = x_scaled
# x = obesity.drop('0be1dad', axis=1)
x.head(), y.head()
(     Gender       Age    Height    Weight  family_history_with_overweight  \0  1.004152  0.105699 -0.002828 -0.235713                        0.469099   1 -0.995866 -1.027052 -1.606291 -1.170931                        0.469099   2 -0.995866 -1.027052  0.128451 -1.430012                        0.469099   3 -0.995866 -0.507929  0.120090  1.644770                        0.469099   4  1.004152  1.371197  2.450367  0.224054                        0.469099   FAVC      FCVC       NCP      CAEC     SMOKE      CH2O       SCC  \0  0.30588 -0.836279  0.314684  0.381571 -0.109287  1.206594 -0.185009   1  0.30588 -0.836279  0.338364 -1.475555 -0.109287 -0.048349 -0.185009   2  0.30588 -1.060332 -1.913423  0.381571 -0.109287 -0.195644 -0.185009   3  0.30588  1.039171  0.338364  0.381571 -0.109287 -0.584035 -0.185009   4  0.30588  0.438397 -1.119801  0.381571 -0.109287 -0.081469 -0.185009   FAF       TUE      CALC    MTRANS   0be1dad  0 -1.171141  0.597438  0.605072  0.429319  1.574360  1  0.021775  0.636513 -1.709084 -2.182324 -1.537581  2 -0.138022  1.755239 -1.709084  0.429319 -1.018924  3  0.579896  0.271455  0.605072  0.429319  0.537046  4  1.176486  0.523111  0.605072  0.429319  1.574360  ,id0    61    02    13    44    6Name: 0be1dad, dtype: int32)
训练模型
# 7. 划分训练集和测试集 7:3x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=42)
x_train.head(), x_test.head(), y_train.head(), y_test.head()
(         Gender       Age    Height    Weight  family_history_with_overweight  \1846   1.004152  1.434298  0.569868  1.217350                        0.469099   14225 -0.995866 -0.713540  1.401081  1.746285                        0.469099   9438   1.004152 -0.360075  0.878986  0.079838                        0.469099   12459 -0.995866 -0.499620 -0.346409 -0.829748                        0.469099   12189 -0.995866  0.286369 -0.825107  0.738886                        0.469099   FAVC      FCVC       NCP      CAEC     SMOKE      CH2O       SCC  \1846  -3.26926 -0.836279  0.338364  0.381571 -0.109287 -1.691863 -0.185009   14225  0.30588  1.039171  0.338364  0.381571 -0.109287  1.325007 -0.185009   9438   0.30588  0.891433  0.314548  0.381571 -0.109287  0.255443 -0.185009   12459  0.30588 -0.836279  0.338364  0.381571 -0.109287 -0.048349 -0.185009   12189  0.30588  1.039171  0.338364  0.381571 -0.109287  0.862169 -0.185009   FAF       TUE      CALC    MTRANS   0be1dad  1846  -1.171141  2.297369  0.605072 -2.182324  0.018390  14225  0.803717  0.332553  0.605072  0.429319  0.537046  9438   1.410892 -1.024344  0.605072  0.429319  1.055703  12459  0.021775  2.297369  0.605072  0.429319 -1.537581  12189 -1.140380 -0.220215  0.605072  0.429319  0.537046  ,Gender       Age    Height    Weight  family_history_with_overweight  \10317 -0.995866  0.379434 -0.584893  0.911536                        0.469099   4074   1.004152 -1.027052  0.569868 -0.299019                       -2.131745   9060  -0.995866 -0.084652  0.150442 -0.120003                        0.469099   11286  1.004152  1.083034 -0.338770  0.914090                        0.469099   8254   1.004152 -1.202863 -1.033617 -1.436296                       -2.131745   FAVC      FCVC       NCP      CAEC     SMOKE      CH2O       SCC  \10317  0.30588  1.039171  0.338364  0.381571 -0.109287 -1.211170 -0.185009   4074   0.30588 -0.836279  0.338364  0.381571 -0.109287 -0.048349 -0.185009   9060   0.30588  0.814419  0.338364  0.381571 -0.109287  1.344141 -0.185009   11286  0.30588 -1.638904  0.338364  0.381571 -0.109287 -0.042493 -0.185009   8254   0.30588 -0.836279  0.338364  0.381571 -0.109287 -0.048349 -0.185009   FAF       TUE      CALC    MTRANS   0be1dad  10317 -1.093287  0.157075  0.605072  0.429319  0.537046  4074   0.021775  0.636513  0.605072  0.429319  1.055703  9060   1.214691 -1.020025 -1.709084  0.429319 -0.500267  11286 -1.171141 -0.834247  0.605072  0.429319  0.018390  8254  -1.171141  2.297369 -1.709084  0.429319 -1.018924  ,id1846     314225    49438     512459    012189    4Name: 0be1dad, dtype: int32,id10317    44074     59060     211286    38254     1Name: 0be1dad, dtype: int32)
# 8. 构建SVM 模型svc = svm.SVC(kernel='linear', C=1, gamma='auto')
svc.fit(x_train, y_train)
SVC(C=1, gamma='auto', kernel='linear')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SVC(C=1, gamma='auto', kernel='linear')
如何确定核函数?
# 8.1 在遇到完全陌生的数据集,如何选择 SVM 模型最优核函数?当然是最简单的方法,对不同的核函数进行尝试,根据最后计算得分最高的就是最优核函数# 定义不同的核函数和参数进行尝试
kernels = ['linear', 'rbf', 'poly', 'sigmoid'] # sigmoid运行的很慢,需要将数据进行标准化
# kernels = ['linear', 'rbf', 'poly']# 存储每个参数组合的交叉验证分数
scores = []for kernel in kernels:print(kernel)svc = svm.SVC(kernel=kernel, C=1, gamma='auto')scores.append(cross_val_score(svc, x_train, y_train, cv=5).mean())scores
linear
rbf
poly
sigmoid[0.999587061252581, 0.9905024088093599, 0.9889194769442533, 0.7741913282863042]

综上:模型使用rbf核函数效果最好

sklearn.svm.SVC() 支持向量机参数详解,用法如下:

class sklearn.svm.SVC(*, C=1.0, kernel='rbf', degree=3, gamma='scale', coef0=0.0, shrinking=True, probability=False, tol=0.001, cache_size=200, class_weight=None, verbose=False, max_iter=-1, decision_function_shape='ovr', break_ties=False, random_state=None)  

挑选重要性较大的几个参数说明:

  • C:浮点数,可不填,默认1.0,必须大于等于 0,为松弛系数的惩罚项系数。
    如果 C 值设定比较大,那 SVC 可能会选择边际较小的,能够更好地分类所有训练点的决策边界。如果 C 的设定值较小,那 SVC 会尽量最大化边界,决策功能会更简单, 但代价是训练的准确度。 总而言之, CSVM 中的影响就像正则化参数对逻辑回归的影响。
  • kernel:字符,可不填,默认 rbf。指定要在算法中使用的核函数类型,可以输入 linear, poly, rbf, sigmoid
  • degree:整数,可不填,默认 3。多项式核函数的次数(poly) ,如果核函数没有选择 poly,这个参数会被忽略。
  • gamma:浮点数,可不填,默认 auto。核函数的系数,仅在参数 Kernel 的选项为 rbfpolysigmoid 的时候有效。当输入 auto, 自动使用 1/(n_features)作为 gamma 的取值。
  • coefo:浮点数,可不填,默认 0.0。核函数中的独立项,它只在参数 kernelpolysigmoid 的时候有效。

这段代码尝试使用不同的核函数和参数配置来训练支持向量机(SVM)模型,并使用交叉验证评估每个配置的性能。具体步骤如下:

代码解析

# 导入所需的库
from sklearn import svm
from sklearn.model_selection import cross_val_score

定义核函数和初始化参数

# 定义要尝试的不同核函数
kernels = ['linear', 'rbf', 'poly', 'sigmoid']# 用于存储每个参数组合的交叉验证分数
scores = []
  • kernels 列表定义了四种不同的核函数:线性核(linear)、径向基核(rbf)、多项式核(poly)和 Sigmoid 核(sigmoid)。
  • scores 列表用于存储每种核函数对应的交叉验证分数的平均值。

迭代不同核函数,训练模型并评估

for kernel in kernels:# 使用当前核函数定义 SVM 分类器svc = svm.SVC(kernel=kernel, C=1, gamma='auto')# 对当前分类器进行交叉验证,cv=5 表示使用 5 折交叉验证cv_scores = cross_val_score(svc, x_train, y_train, cv=5)# 计算交叉验证分数的平均值并存储在 scores 列表中scores.append(cv_scores.mean())
  • for kernel in kernels::遍历每种核函数。
  • svc = svm.SVC(kernel=kernel, C=1, gamma='auto'):定义 SVM 分类器,设置当前核函数、惩罚参数 C=1,以及核系数 gamma='auto'
    • kernel=kernel:设置当前的核函数。
    • C=1:惩罚参数,较小的 C 值会使模型更宽松,较大的 C 值会使模型对错误分类更敏感。
    • gamma='auto':核系数,对于 RBF、多项式和 Sigmoid 核函数有影响。设置为 ‘auto’ 时,gamma 值为 1 / n_features
  • cv_scores = cross_val_score(svc, x_train, y_train, cv=5):对当前分类器进行 5 折交叉验证,返回每次验证的分数。
  • scores.append(cv_scores.mean()):计算交叉验证分数的平均值,并存储在 scores 列表中。

输出交叉验证分数

scores
  • 最终,scores 列表包含了每种核函数对应的交叉验证分数的平均值。这些分数可以用于比较不同核函数在该数据集上的表现。

总结

这段代码通过定义不同的核函数并进行交叉验证来评估每种核函数的性能。最终,scores 列表包含了每种核函数在 5 折交叉验证中的平均得分,便于比较和选择最佳的核函数。

重新建立模型预测测试集并计算指标
# 9. 预测测试集
svc = svm.SVC(kernel='rbf', C=1, gamma='auto')
svc.fit(x_train, y_train)y_pred = svc.predict(x_test)
y_pred
array([4, 5, 2, ..., 1, 2, 1])
# 10. 计算测试集的平均准确率和预测测试集准确率acc = accuracy_score(y_test, y_pred)svc.score(x_test, y_test), acc
(0.9932562620423893, 0.9932562620423893)
总结

SVM 支持向量机采用扩展维度空间的方式进行分类,从而避免了之前逻辑回归的二维空间内的问题(线性不可分)。SVM 在扩展维度空间后,即当前数据线性可分,通过计算间隔最大化的分离超平面将数据分开,其对未知数据的预测性是最强的。

课后思考题

  1. 当训练数据线性不可分时,为何引入核函数就可以可分?(对偶问题)

闯关题

Q1. svm中的核函数下面解释正确的是?(多选题)
A. linear 线性分类核函数,适用于线性可分的数据。
B. poly 适用于非线性的问题。
C. rbf 适用于线性不可分的数据。
D. sigmoid 适用于需要逻辑回归模型的决策边界。

关于支持向量机(SVM)中的核函数,下面的解释中哪些是正确的?(多选题)

A. linear 线性分类核函数,适用于线性可分的数据。
B. poly 适用于非线性的问题。
C. rbf 适用于线性不可分的数据。
D. sigmoid 适用于需要逻辑回归模型的决策边界。

下面是对每个选项的解释:

A. linear 线性分类核函数,适用于线性可分的数据。

  • 正确。线性核函数适用于线性可分的数据集,即数据可以通过一个线性决策边界分开。

B. poly 适用于非线性的问题。

  • 正确。多项式核函数(poly)适用于非线性的问题,通过增加特征空间的维度使得线性不可分的数据在高维空间中变得可分。

C. rbf 适用于线性不可分的数据。

  • 正确。径向基函数核(rbf)适用于线性不可分的数据,通过映射到高维空间,可以使得原本线性不可分的数据在高维空间中变得可分。

D. sigmoid 适用于需要逻辑回归模型的决策边界。

  • 正确。Sigmoid 核函数与神经网络中的激活函数类似,适用于某些类型的二分类问题,并可以产生类似于逻辑回归的决策边界。

综上所述,正确的答案是:
A, B, C, D

Q2. SVM出现欠拟合时,下面哪些可以解决?(欠拟合是指模型不能在训练集上获得足够低的误差,即模型训练较少,没有充分从训练集中学习到规律。)
A. 增大惩罚参数 C 的值
B. 减小惩罚参数 C 的值
C. 减小核系数(gamma参数)

SVM 出现欠拟合时,以下哪些可以解决问题?(欠拟合是指模型不能在训练集上获得足够低的误差,即模型训练较少,没有充分从训练集中学习到规律。)

A. 增大惩罚参数 C 的值
B. 减小惩罚参数 C 的值
C. 减小核系数(gamma 参数)

解释

  • A. 增大惩罚参数 C 的值:

    • 正确。惩罚参数 C 控制的是对误分类的惩罚程度。增大 C 的值会使模型对训练集中的误分类样本惩罚更大,从而使模型更关注训练数据,减少欠拟合的可能性。
  • B. 减小惩罚参数 C 的值:

    • 不正确。减小 C 的值会使模型对误分类样本的惩罚变小,导致模型更加宽容,从而可能增加欠拟合的风险。
  • C. 减小核系数(gamma 参数):

    • 不正确。减小 gamma 的值会使得 RBF 核函数的作用范围变大,从而可能导致模型变得更简单,更容易出现欠拟合。相反,增大 gamma 的值可以使模型变得更加复杂,减少欠拟合。

综上所述,正确的解决方案是:
A. 增大惩罚参数 C 的值

Q3. 使用iris 数据集基于SVM 训练模型计算其准确率是多少?
A. 1
B. 0.99
C. 0.98
D. 0.97

from sklearn.datasets import load_iris
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn import svmiris = load_iris()
x, y = iris.data, iris.target
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=42)svc = svm.SVC(kernel='linear', C=1, gamma='auto')
svc.fit(x_train, y_train)
y_pred = svc.predict(x_test)acc = accuracy_score(y_test, y_pred)
acc
1.0
#填入你的答案并运行,注意大小写
a1 = 'ABCD'  # 如 a1= 'A'
a2 = 'A'  # 如 a2= 'A'
a3 = 'A'  # 如 a3= 'A'

http://www.lryc.cn/news/417731.html

相关文章:

  • AutoGPT项目实操总结
  • uniapp 荣耀手机 没有检测到设备 运行到Android手机 真机运行
  • 【EtherCAT】Windows+Visual Studio配置SOEM主站——静态库配置+部署
  • 【Python小游戏示例:猜拳游戏】
  • 多态实现的必要条件,实现多态的三个方法,输入一个URL的过程,死锁产生的原理和条件,进程和线程的定义及区别,进程通信的几种方式
  • Springboot+MybatisPlus项目中,数据库表中存放Date,查出后转为String
  • JavaDS —— AVL树
  • NSSCTF练习记录:[SWPUCTF 2021 新生赛]jicao
  • LabVIEW位移检测系统
  • 02、MySQL-DML(数据操作语言)
  • vue3 项目部署到线上环境,初始进入系统,页面卡顿大概一分钟左右,本地正常无卡顿。localStorage缓存1MB数据导致页面卡顿。
  • 软件更新中的风险识别与质量保证机制分析
  • QT下载与安装
  • Java 2.2 - Java 集合
  • Linux驱动.之I2C,iic驱动层(二)
  • 【STM32】USART串口和I2C通信
  • 【Material-UI】按钮组:垂直按钮组详解
  • DDR5 的优势与应用
  • STM32 - 笔记
  • 基于QT实现的简易WPS(已开源)
  • Flask-WTF 表单处理详细教程(第六阶段)
  • C语言 | Leetcode C语言题解之第330题按要求补齐数组
  • 无人机之测绘行业篇
  • Java编程:每日挑战
  • 【自动驾驶】ubuntu server安装桌面版
  • 前端模块化-手写mini-vite
  • SpringBoot中fastjson扩展: 自定义序列化和反序列化方法实战
  • 【QT】鼠标按键事件 - QMouseEvent QKeyEvent
  • 纯手工在内网部署一个Docker私有仓库
  • 农林经济管理学报