小标
2019-01-11
来源 :
阅读 1277
评论 0
摘要:本文主要向大家介绍了机器学习入门之《Scikit-Learn与TensorFlow机器学习实用指南》 第02章,通过具体的内容向大家展现,希望对大家学习机器学习入门有所帮助。
本文主要向大家介绍了机器学习入门之《Scikit-Learn与TensorFlow机器学习实用指南》 第02章,通过具体的内容向大家展现,希望对大家学习机器学习入门有所帮助。
数据清洗
大多机器学习算法不能处理特征丢失,因此先创建一些函数来处理特征丢失的问题。前面,你应该注意到了属性total_bedrooms有一些缺失值。有三个解决选项:
去掉对应的分区;
去掉整个属性;
进行赋值(0、平均值、中位数等等)。
用DataFrame的dropna(), drop(),和 fillna()方法,可以方便地实现:
housing.dropna(subset=["total_bedrooms"]) # 选项1
housing.drop("total_bedrooms", axis=1) # 选项2
median = housing["total_bedrooms"].median()
housing["total_bedrooms"].fillna(median) # 选项3
如果选择选项3,你需要计算训练集的中位数,用中位数填充训练集的缺失值,不要忘记保存该中位数。后面用测试集评估系统时,需要替换测试集中的缺失值,也可以用来实时替换新数据中的缺失值。
Scikit-Learn提供了一个方便的类来处理缺失值:Imputer。下面是其使用方法:首先,需要创建一个Imputer实例,指定用该属性的中位数替换它的每个缺失值:
from sklearn.preprocessing import Imputer
imputer = Imputer(strategy="median")
因为只有数值属性才能算出中位数,我们需要创建一份不包括文本属性ocean_proximity的数据副本:
housing_num = housing.drop("ocean_proximity", axis=1)
现在,就可以用fit()方法将imputer实例拟合到训练数据:
imputer.fit(housing_num)
imputer计算出了每个属性的中位数,并将结果保存在了实例变量statistics_中。只有属性total_bedrooms有缺失值,但是我们要确保一旦系统运行起来,新的数据中没有缺失值,所以安全的做法是将imputer应用到每个数值:
>>> imputer.statistics_
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])
>>> housing_num.median().values
array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])
现在,你就可以使用这个“训练过的”imputer来对训练集进行转换,通过将缺失值替换为中位数:
X = imputer.transform(housing_num)
结果是一个普通的Numpy数组,包含有转换后的特征。如果你想将其放回到Pandas DataFrame中,也很简单:
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
Scikit-Learn设计
Scikit-Learn设计的API设计的非常好。它的主要设计原则是:
一致性:所有对象的接口一致且简单:
估计量(estimator)。任何可以基于数据集而对一些参数进行估计的对象都被设计成估计量(比如,imputer就是个估计量)。估计本身是通过fit()方法,只需要一个数据集作为参数(对于监督学习算法,需要两个数据集;第二个数据集包含标签)。任何其它用来指导估计过程的参数都被当做超参数(比如imputer的strategy),并且超参数要被设置成实例变量(通常是通过构造器参数)。
转换量(transformer)。一些估计量(比如imputer)也可以转换数据集,这些估计量被称为转换量。API也是相当简单:转换是通过transform()方法,被转换的数据集作为参数。返回的是经过转换的数据集。转换过程依赖学习到的参数,比如imputer的例子。所有的转换都有一个便捷的方法fit_transform(),等同于调用fit()再transform()(但有时fit_transform()经过优化,运行的更快)。
预测量(predictor)。最后,一些估计量可以根据给出的数据集做预测,这些估计量称为预测量。例如,上一章的LinearRegression模型就是一个预测量:它根据一个国家的人均GDP预测生活满意度。预测量有一个predict()方法,可以用新实例的数据集做出相应的预测。预测量还有一个score()方法,可以根据测试集(和相应的标签,如果是监督学习算法的话)对预测进行衡量。
可检验。所有估计量的超参数都可以通过公共实例变量直接访问(比如,imputer.strategy),并且所有估计量学习到的参数也可以通过公共实例变量添加下划线后缀访问(比如,imputer.statistics_)。
类不可扩散。数据集被表示成NumPy数组或SciPy稀疏矩阵,而不是自制的类。超参数只是普通的Python字符串或数字。
可组合。尽可能使用现存的模块。例如,用任意的转换量序列加上一个估计量,就可以做成一个Pipeline,后面会看到例子。
合理的默认值。Scikit-Learn给大多数参数提供了合理的默认值,很容易就能创建一个系统。
处理文本和分类属性
前面,我们丢弃了分类属性ocean_proximity,因为它是一个文本属性,不能计算出中位数。大多数机器学习算法更喜欢和数字打交道,所以让我们把这些文本标签转换为数字。
Scikit-Learn为这个任务提供了一个转换量LabelEncoder:
>>> from sklearn.preprocessing import LabelEncoder
>>> encoder = LabelEncoder()
>>> housing_cat = housing["ocean_proximity"]
>>> housing_cat_encoded = encoder.fit_transform(housing_cat)
>>> housing_cat_encoded
array([1, 1, 4, ..., 1, 0, 3])
好了一些,现在就可以在任何ML算法里用这个数值数据了。你可以查看映射表,编码器是通过属性classes_来学习的(“<1H OCEAN”被映射为0,“INLAND”被映射为1,等等):
>>> print(encoder.classes_)
['<1H OCEAN' 'INLAND' 'ISLAND' 'NEAR BAY' 'NEAR OCEAN']
这种做法的问题是,ML算法会认为两个临近的值比两个疏远的值要更相似。显然这样不对(比如,分类0和4比0和1更相似)。要解决这个问题,一个常见的方法是给每个分类创建一个二元属性:当分类是“<1H OCEAN”,该属性为1(否则为0),当分类是“INLAND”,另一个属性等于1(否则为0),以此类推。这称作独热编码(One-Hot Encoding),因为只有一个属性会等于1(热),其余会是0(冷)。
Scikit-Learn提供了一个编码器OneHotEncoder,用于将整书分类值转变为独热矢量。注意fit_transform()用于2D数组,而housing_cat_encoded是一个1D数组,所以需要将其变形:
>>> from sklearn.preprocessing import OneHotEncoder
>>> encoder = OneHotEncoder()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
>>> housing_cat_1hot
<16513x5 sparse matrix of type '<class 'numpy.float64'>'
with 16513 stored elements in Compressed Sparse Row format>
注意输出结果是一个SciPy稀疏矩阵,而不是NumPy数组。当分类属性有数千个分类时,这样非常有用。经过独热编码,我们得到了一个有数千列的矩阵,这个矩阵每行只有一个1,其余都是0。使用大量内存来存储这些0非常浪费,所以稀疏矩阵只存储非零元素的位置。你可以像一个2D数据那样进行使用,但是如果你真的想将其转变成一个(密集的)NumPy数组,只需调用toarray()方法:
>>> housing_cat_1hot.toarray()
array([[ 0., 1., 0., 0., 0.],
[ 0., 1., 0., 0., 0.],
[ 0., 0., 0., 0., 1.],
...,
[ 0., 1., 0., 0., 0.],
[ 1., 0., 0., 0., 0.],
[ 0., 0., 0., 1., 0.]])
使用类LabelBinarizer,我们可以用一步执行这两个转换(从文本分类到整数分类,再从整数分类到独热矢量):
>>> from sklearn.preprocessing import LabelBinarizer
>>> encoder = LabelBinarizer()
>>> housing_cat_1hot = encoder.fit_transform(housing_cat)
>>> housing_cat_1hot
array([[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 1],
...,
[0, 1, 0, 0, 0],
[1, 0, 0, 0, 0],
[0, 0, 0, 1, 0]])
注意默认返回的结果是一个密集NumPy数组。向构造器LabelBinarizer传递sparse_output=True,就可以得到一个稀疏矩阵。
自定义转换量
尽管Scikit-Learn提供了许多有用的转换量,你还是需要自己动手写转换量执行任务,比如自定义的清理操作,或属性组合。你需要让自制的转换量与Scikit-Learn组件(比如pipeline)无缝衔接工作,因为Scikit-Learn是依赖鸭子类型的(而不是继承),你所需要做的是创建一个类并执行三个方法:fit()(返回self),transform(),和fit_transform()。通过添加TransformerMixin作为基类,可以很容易地得到最后一个。另外,如果你添加BaseEstimator作为基类(且构造器中避免使用*args和**kargs),你就能得到两个额外的方法(get_params()和set_params()),二者可以方便地进行超参数自动微调。例如,一个小转换量类添加了上面讨论的属性:
from sklearn.base import BaseEstimator, TransformerMixin
rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
def __init__(self, add_bedrooms_per_room = True): # no *args or **kargs
self.add_bedrooms_per_room = add_bedrooms_per_room
def fit(self, X, y=None):
return self # nothing else to do
def transform(self, X, y=None):
rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
population_per_household = X[:, population_ix] / X[:, household_ix]
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
return np.c_[X, rooms_per_household, population_per_household,
bedrooms_per_room]
else:
return np.c_[X, rooms_per_household, population_per_household]
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)
在这个例子中,转换量有一个超参数add_bedrooms_per_room,默认设为True(提供一个合理的默认值很有帮助)。这个超参数可以让你方便地发现添加了这个属性是否对机器学习算法有帮助。更一般地,你可以为每个不能完全确保的数据准备步骤添加一个超参数。数据准备步骤越自动化,可以自动化的操作组合就越多,越容易发现更好用的组合(并能节省大量时间)。
特征缩放
数据要做的最重要的转换之一是特征缩放。除了个别情况,当输入的数值属性量度不同时,机器学习算法的性能都不会好。这个规律也适用于房产数据:总房间数分布范围是6到39320,而收入中位数只分布在0到15。不需要对目标值进行缩放。
有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min-Max scaling)和标准化(standardization)。
线性函数归一化(许多人称其为归一化(normalization))很简单:值被转变、重新缩放,直到范围变成0到1。我们通过减去最小值,然后再除以最大值与最小值的差值,来进行归一化。Scikit-Learn提供了一个转换量MinMaxScaler来实现这个功能。它有一个超参数feature_range,可以让你改变范围,如果不希望范围是0到1。
标准化就很不同:首先减去平均值(所以标准化值的平均值总是0),然后除以方差,使得到的分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围,这对某些算法可能构成问题(比如,神经网络常需要输入值得范围是0到1)。但是,标准化受到异常值的影响很小。例如,假设一个分区的收入中位数是100。归一化会将其它范围是0到15的值变为0-0.15,但是标准化不会受什么影响。Scikit-Learn提供了一个转换量StandardScaler来进行标准化。
警告:与所有的转换一样,缩放器只能向训练集拟合,而不是向完整的数据集(包括测试集)。只有这样,才能用缩放器转换训练集和测试集(和新数据)。
转换Pipeline
你已经看到,存在许多数据转换步骤,需要按一定的顺序执行。幸运的是,Scikit-Learn提供了类Pipeline,来进行这一系列的转换。下面是一个数值属性的小pipeline:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
('imputer', Imputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
Pipeline构造器需要一个定义步骤顺序的名字/估计量对的列表。除了最后一个估计量,其余都要是转换量(即,它们都要有fit_transform()方法)。名字可以随意起。
当你调用pipeline的fit()方法,就会对所有转换量顺序调用fit_transform()方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个评估量,它只执行fit()方法。
pipeline暴露相同的方法作为最终的评估量。在这个例子中,最后的评估量是一个StandardScaler,它是一个转换量,因此这个pipeline有一个transform()方法,可以顺序对数据做所有转换(它还有一个fit_transform方法可以使用,就不必先调用fit()再进行transform())。
你现在就有了一个对数值的pipeline,你还需要对分类值应用LabelBinarizer:如何将这些转换写成一个pipeline呢?Scikit-Learn提供了一个类FeatureUnion实现这个功能。你给它一列转换量(可以是所有的转换量),当调用它的transform()方法,每个转换量的transform()会被并行执行,等待输出,然后将输出合并起来,并返回结果(当然,调用它的fit()方法就会调用每个转换量的fit())。一个完整的处理数值和分类属性的pipeline如下所示:
from sklearn.pipeline import FeatureUnion
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]
num_pipeline = Pipeline([
('selector', DataFrameSelector(num_attribs)),
('imputer', Imputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
cat_pipeline = Pipeline([
('selector', DataFrameSelector(cat_attribs)),
('label_binarizer', LabelBinarizer()),
])
full_pipeline = FeatureUnion(transformer_list=[
("num_pipeline", num_pipeline),
("cat_pipeline", cat_pipeline),
])
你可以很简单地运行整个pipeline:
>>> housing_prepared = full_pipeline.fit_transform(housing)
>>> housing_prepared
array([[ 0.73225807, -0.67331551, 0.58426443, ..., 0. ,
0. , 0. ],
[-0.99102923, 1.63234656, -0.92655887, ..., 0. ,
0. , 0. ],
[...]
>>> housing_prepared.shape
(16513, 17)
每个子pipeline都以一个选择转换量开始:通过选择对应的属性(数值或分类)、丢弃其它的,来转换数据,并将输出DataFrame转变成一个NumPy数组。Scikit-Learn没有工具来处理Pandas DataFrame,因此我们需要写一个简单的自定义转换量来做这项工作:
from sklearn.base import BaseEstimator, TransformerMixin
class DataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attribute_names):
self.attribute_names = attribute_names
def fit(self, X, y=None):
return self
def transform(self, X):
return X[self.attribute_names].values
选择并训练模型
可到这一步了!你在前面限定了问题、获得了数据、探索了数据、采样了一个测试集、写了自动化的转换pipeline来清理和为算法准备数据。现在,你已经准备好选择并训练一个机器学习模型了。
在训练集上训练和评估
好消息是基于前面的工作,接下来要做的比你想的要简单许多。像前一章那样,我们先来训练一个线性回归模型:
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
完毕!你现在就有了一个可用的线性回归模型。用一些训练集中的实例做下验证:
>>> some_data = housing.iloc[:5]
>>> some_labels = housing_labels.iloc[:5]
>>> some_data_prepared = full_pipeline.transform(some_data)
>>> print("Predictions:\t", lin_reg.predict(some_data_prepared))
Predictions: [ 303104. 44800. 308928. 294208. 368704.]
>>> print("Labels:\t\t", list(some_labels))
Labels: [359400.0, 69700.0, 302100.0, 301300.0, 351900.0]
行的通,尽管预测并不怎么准确(比如,第二个预测偏离了50%!)。让我们使用Scikit-Learn的mean_squared_error函数,用全部训练集来计算下这个回归模型的RMSE:
>>> from sklearn.metrics import mean_squared_error
>>> housing_predictions = lin_reg.predict(housing_prepared)
>>> lin_mse = mean_squared_error(housing_labels, housing_predictions)
>>> lin_rmse = np.sqrt(lin_mse)
>>> lin_rmse
68628.413493824875
okay,有总比没有强,但显然结果并不好:大多数分区的median_housing_values位于120000美元到265000美元之间,因此预测误差$68628不能让人满意。这是一个模型欠拟合训练数据的例子。当这种情况发生时,意味着特征没有提供足够多的信息来做出一个好的预测,或者模型并不强大。就像前一章看到的,修复欠拟合的主要方法是选择一个更强大的模型,给训练算法提供更好的特征,或去掉模型上的限制。这个模型还没有正则化,所以排除了最后一个选项。你可以尝试添加更多特征(比如,人口的对数值),但是首先让我们尝试一个更为复杂的模型,看看效果。
来训练一个DecisionTreeRegressor。这是一个强大的模型,可以发现数据中复杂的非线性关系(决策树会在第6章详细讲解)。代码看起来很熟悉:
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)
现在模型就训练好了,用训练集评估下:
>>> housing_predictions = tree_reg.predict(housing_prepared)
>>> tree_mse = mean_squared_error(housing_labels, housing_predictions)
>>> tree_rmse = np.sqrt(tree_mse)
>>> tree_rmse
0.0
等一下,发生了什么?没有误差?这个模型可能是绝对完美的吗?当然,更大可能性是这个模型严重过拟合数据。如何确定呢?如前所述,直到你准备运行一个具备足够信心的模型,都不要碰测试集,因此你需要使用训练集的部分数据来做训练,用一部分来做模型验证。
使用交叉验证做更佳的评估
评估决策树模型的一种方法是用函数train_test_split来分割训练集,得到一个更小的训练集和一个验证集,然后用更小的训练集来训练模型,用验证集来评估。这需要一定工作量,并不难而且也可行。
另一种更好的方法是使用Scikit-Learn的交叉验证功能。下面的代码采用了K折交叉验证(K-fold cross-validation):它随机地将训练集分成十个不同的子集,成为“折”,然后训练评估决策树模型10次,每次选一个不用的折来做评估,用其它9个来做训练。结果是一个包含10个评分的数组:
from sklearn.model_selection import cross_val_score
scores = cross_val_score(tree_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=10)
rmse_scores = np.sqrt(-scores)
警告:Scikit-Learn交叉验证功能期望的是效用函数(越大越好)而不是成本函数(越低越好),因此得分函数实际上与MSE相反(即负值),这就是为什么前面的代码在计算平方根之前先计算-scores。
来看下结果:
>>> def display_scores(scores):
... print("Scores:", scores)
... print("Mean:", scores.mean())
... print("Standard deviation:", scores.std())
...
>>> display_scores(tree_rmse_scores)
Scores: [ 74678.4916885 64766.2398337 69632.86942005 69166.67693232
71486.76507766 73321.65695983 71860.04741226 71086.32691692
76934.2726093 69060.93319262]
Mean: 71199.4280043
Standard deviation: 3202.70522793
现在决策树就不像前面看起来那么好了。实际上,它看起来比线性回归模型还糟!注意到交叉验证不仅可以让你得到模型性能的评估,还能测量评估的准确性(即,它的标准差)。决策树的评分大约是71200,通常波动有±3200。如果只有一个验证集,就得不到这些信息。但是交叉验证的代价是训练了模型多次,不可能总是这样。
让我们计算下线性回归模型的的相同分数,以做确保:
>>> lin_scores = cross_val_score(lin_reg, housing_prepared, housing_labels,
... scoring="neg_mean_squared_error", cv=10)
...
>>> lin_rmse_scores = np.sqrt(-lin_scores)
>>> display_scores(lin_rmse_scores)
Scores: [ 70423.5893262 65804.84913139 66620.84314068 72510.11362141
66414.74423281 71958.89083606 67624.90198297 67825.36117664
72512.36533141 68028.11688067]
Mean: 68972.377566
Standard deviation: 2493.98819069
判断没错:决策树模型过拟合很严重,它的性能比线性回归模型还差。
现在再尝试最后一个模型:RandomForestRegressor。第7章我们会看到,随机森林是通过用特征的随机子集训练许多决策树。在其它多个模型之上建立模型成为集成学习(Ensemble Learning),它是推进ML算法的一种好方法。我们会跳过大部分的代码,因为代码本质上和其它模型一样:
>>> from sklearn.ensemble import RandomForestRegressor
>>> forest_reg = RandomForestRegressor()
>>> forest_reg.fit(housing_prepared, housing_labels)
>>> [...]
>>> forest_rmse
22542.396440343684
>>> display_scores(forest_rmse_scores)
Scores: [ 53789.2879722 50256.19806622 52521.55342602 53237.44937943
52428.82176158 55854.61222549 52158.02291609 50093.66125649
53240.80406125 52761.50852822]
Mean: 52634.1919593
Standard deviation: 1576.20472269
现在好多了:随机森林看起来很有希望。但是,训练集的评分仍然比验证集的评分低很多。解决过拟合可以通过简化模型,给模型加限制(即,规整化),或用更多的训练数据。在深入随机森林之前,你应该尝试下机器学习算法的其它类型模型(不同核心的支持向量机,神经网络,等等),不要在调节超参数上花费太多时间。目标是列出一个可能模型的列表(两到五个)。
提示:你要保存每个试验过的模型,以便后续可以再用。要确保有超参数和训练参数,以及交叉验证评分,和实际的预测值。这可以让你比较不同类型模型的评分,还可以比较误差种类。你可以用Python的模块pickle,非常方便地保存Scikit-Learn模型,或使用sklearn.externals.joblib,后者序列化大NumPy数组更有效率:
from sklearn.externals import joblib
joblib.dump(my_model, "my_model.pkl")
# 然后
my_model_loaded = joblib.load("my_model.pkl")
模型微调
假设你现在有了一个列表,列表里有几个有希望的模型。你现在需要对它们进行微调。让我们来看几种微调的方法。
网格搜索
微调的一种方法是手工调整超参数,直到找到一个好的超参数组合。这么做的话会非常冗长,你也可能没有时间探索多种组合。
你应该使用Scikit-Learn的GridSearchCV来做这项搜索工作。你所需要做的是告诉GridSearchCV要试验有哪些超参数,要试验什么值,GridSearchCV就能用交叉验证试验所有可能超参数值的组合。例如,下面的代码搜索了RandomForestRegressor超参数值的最佳组合:
from sklearn.model_selection import GridSearchCV
param_grid = [
{'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
{'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
]
forest_reg = RandomForestRegressor()
grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
scoring='neg_mean_squared_error')
grid_search.fit(housing_prepared, housing_labels)
当你不能确定超参数该有什么值,一个简单的方法是尝试连续的10的次方(如果想要一个粒度更小的搜寻,可以用更小的数,就像在这个例子中对超参数n_estimators做的)。
param_grid告诉Scikit-Learn首先评估所有的列在第一个dict中的n_estimators和max_features的3 × 4 = 12种组合(不用担心这些超参数的含义,会在第7章中解释)。然后尝试第二个dict中超参数的2 × 3 = 6种组合,这次会将超参数bootstrap设为False而不是True(后者是该超参数的默认值)。
总之,网格搜索会探索12 + 6 = 18种RandomForestRegressor的超参数组合,会训练每个模型五次(因为用的是五折交叉验证)。换句话说,训练总共有18 × 5 = 90轮!折将要花费大量时间,完成后,你就能获得参数的最佳组合,如下所示:
>>> grid_search.best_params_
{'max_features': 6, 'n_estimators': 30}
提示:因为30是n_estimators的最大值,你也应该估计更高的值,因为这个值会持续提升。
你还能直接得到最佳的估计量:
>>> grid_search.best_estimator_
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
max_features=6, max_leaf_nodes=None, min_
本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标人工智能机器学习频道!
喜欢 | 0
不喜欢 | 0
您输入的评论内容中包含违禁敏感词
我知道了

请输入正确的手机号码
请输入正确的验证码
您今天的短信下发次数太多了,明天再试试吧!
我们会在第一时间安排职业规划师联系您!
您也可以联系我们的职业规划师咨询:
版权所有 职坐标-一站式AI+学习就业服务平台 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
沪公网安备 31011502005948号