发布时间:2023-10-18 10:30
本篇文章主要来记录一下使用机器学习解决我遇到的问题的过程。
目录
一、问题描述
二、数据处理
2.1 缺失值处理
2.2 异常值处理
2.3 特征工程
三、降维
四、模型建立
五、模型训练
首先,先简要描述一下这个问题:题目要求我们进行二手车价格的预测,题中给了data1和data2两个数据集,data1数据集有30000条数据,data2数据集有5000条数据。这两个数据集中都含有36列变量,各变量字段如下图所示:
其中anonymousFeature是匿名变量,一共有15个。数据集data1和data2的不同之处就是data1的price变量有数据,而data2的price是需要我们填写的。也就是说,题目要求我们使用data1数据集来建立模型,用建立好的模型预测data2中的二手车价格。
data1部分数据如下所示:
data2部分数据如下所示:
可以看出,15个匿名变量的话我是对其编了号,从D_0到D_14。data2中price变量没有数据,这是要求我们进行预测的。还要提一句的是,其实变量D_14是有数据的,只不过它缺失值太多,截图的话正好截到了它没有数据的情况。接下来就开始分步骤进行解决。
首先肯定是检查一下数据的缺失值并进行处理。我们先分别读取数据为训练集和测试集,然后将这两个数据集合并:
Train = pd.read_csv('E:\\data1.csv', delimiter=',') # 训练集
Test = pd.read_csv('E:\\data2.csv', delimiter=',') # 测试集
print('Train shape: ', Train.shape)
print('Test shape: ', Test.shape)
Combine = pd.concat([Train, Test]) # 将测试集和训练集合并
print('Combine shape: ', Combine.shape)
上图显示的是训练集和测试集的数据维度,以及合并后的维度。接着,使用isna().sum()函数查看一下数据集各列的缺失值。
print(Combine.isna().sum()) # 统计数据集各列缺失值个数
为了更直观地看出数据确实情况,我将有缺失值的变量跟据缺失值的多少从高到低排列,并进行可视化:
通过上图可以看出,一共有16列变量有缺失值,其中price变量的5000个缺失值是要我们进行预测的,所以不进行额外处理。对于D_14、D_6和D_3,可以看到它们的缺失值过多,所以对模型的训练毫无帮助,甚至可以说是噪音,于是我们将它们删除。对于D_9、D_7、D_8、country、maketype这几列,为了避免对其整体数据的分布产生较大的影响,我们用缺失值的上一个单元格的数据进行填充,如果上一个单元格也是缺失值,那么就用该数据列的众数填充。而对于D_0、D_12、D_10、modelyear、carCode、gearbox、D_11这几列数据,由于缺失值相对较少,所以我们分别用它们对应列的众数填充。
众数填充的话可以使用如下代码操作,方便快捷:
Combine = Combine.fillna(Combine.mode().iloc[0, :]) # 用每一列出现最多的数据填充。
首先,可以使用Combine.info()来查看一下变量的数据类型,这里为了更直观,我还是使用了画图的方法:
如上图所示,33列(上文说过的缺失值较多的3列先不考虑)变量中D_11是object类型,通过查看数据集,我们发现变量D_11的数据如下所示:
我们大概可以猜出,变量D_11表示的是车的三围。所以我们对其进行处理,新建长、宽、高三列变量来拆分变量D_11,代码如下:
# 匿名特征处理:D_11。拆分为length、width和high
series1 = Combine['D_11'].str.split('*', expand=True)
Combine['length'] = series1[0]
Combine['width'] = series1[1]
Combine['high'] = series1[2]
Combine['length'] = Combine['length'].astype(float)
Combine['width'] = Combine['width'].astype(float)
Combine['high'] = Combine['high'].astype(float)
接着,我们再观察数据集。其中,变量carid表示的是车辆的id号,是唯一值,对我们的模型训练没有帮助,可以删掉;匿名变量D_0的话,我们使用Combine.D_0.value_counts()查看该变量值的分布:
由上图可以看出,变量D_0共有1、3、4三个值,但值为一的数据有34935个,占比超过了99%,所以,我们可以说:变量D_0的值的分布极度不平衡,无法为模型训练提供有用信息,可以将其删除。
通过以上分析后,我们将需要删除的变量进行删除操作:
Combine.drop(['D_0', 'D_3', 'carid', 'D_6', 'D_11', 'D_14'], axis=1, inplace=True) # 删除不利于模型训练的变量
其中inplace=True表示的是原数组的值被直接修改。默认的话是inplace=False,它表示的是进行drop操作后将得到一个返回值,这个返回值是修改后的数组,也就是说你要新定义一个变量来接收这个返回值,才能得到修改后的数组。而inplace=True的话呢,就不需要接收返回值,目标数组将会被直接修改。
首先,我们使用Combine.nunique()查看一下各变量不同值的个数:
1.离散特征编码
跟据上图,挑选离散变量'carCode', 'color', 'country', 'maketype', 'oiltype', 'D_7', 'D_8', 'D_9', 'D_10', 'D_13'进行One-hot编码:
定义One-hot编码函数:
def One_Hot(OneHotCol):
new_cols = []
for old_col in OneHotCol:
new_cols += sorted(['{0}_{1}'.format(old_col, str(x).lower()) for x in set(Combine[old_col].values)])
ec = OneHotEncoder()
ec.fit(Combine[OneHotCol].values)
# list(Combine.index.values) # 取出Combine的索引
OneHotCode = pd.DataFrame(ec.transform(Combine[OneHotCol]).toarray(), columns=new_cols,
index=list(Combine.index.values)).astype(int)
return OneHotCode
调用编写的函数,对上述离散变量进行One-hot编码:
OneHotCol = ['carCode', 'color', 'country', 'maketype', 'oiltype', 'D_7', 'D_8', 'D_9', 'D_10', 'D_13']
OneHotCode = One_Hot(OneHotCol)
# 合并Combine和OneHotCode
Combine = pd.concat([Combine, OneHotCode], axis=1)
为什么要使用One-hot编码呢?简单地说,可以理解为:One-hot编码使离散特征的取值扩展到了欧式空间,使离散特征里各类别之间的距离在欧式空间里相等,比普通的用数字进行类别的编码方式更加合理。本代码中,我使用的是sklearn库中preprocessing模块里的OneHotEncoder()函数进行的One-hot编码,所以需要导包。
2.日期特征编码
通过观察数据集,我们发现变量tradeTime、registerDtae、licenseDate、和D_12都表示的是日期信息,所以我们可以考虑将它们转化为日期的标准格式,再提取它们的年、月、日、周几这些特征信息。
日期格式转换函数(将日期转换为xxxx-xx-xx这种标准格式):
def date_proc(x):
month = int(x[4:6])
if month == 0:
month = 1
if len(x) == 6:
return x[:4] + '-' + str(month)
else:
return x[:4] + '-' + str(month) + '-' + x[6:]
日期特征提取函数(提取年、月、日、周几这些特征):
def date_transform(df, fea_col):
for f in tqdm(fea_col):
df[f] = pd.to_datetime(df[f].astype('str').apply(date_proc))
df[f + '_year'] = df[f].dt.year
df[f + '_month'] = df[f].dt.month
df[f + '_day'] = df[f].dt.day
df[f + '_dayofweek'] = df[f].dt.dayofweek
return (df)
其中,tqdm()函数作用是添加进度条,参数是一个可迭代对象。
对'registerDate', 'tradeTime', 'licenseDate'这三个变量进行标准日期格式转换并提取日期特征:
Date = ['registerDate', 'tradeTime', 'licenseDate']
Combine = date_transform(Combine, Date)
由于变量D_12表示的日期信息只有年和月,所以我们对其进行标准日期格式转换后单独对其进行日期特征提取(只提取年和月):
# 匿名特征处理 D_12
Combine = Combine[Combine['D_12'].notna()]
Combine['D_12'].astype('str').apply(date_proc)
Combine['D_12'] = pd.to_datetime(Train['D_12'])
Combine['D_12_year'] = Combine['D_12'].dt.year
Combine['D_12_month'] = Combine['D_12'].dt.month
上述的日期特征提取完毕后,Combine中的这些特征变量名如下所示:
同样,再次调用我们编写的One-hot编码函数,对这些特征进行One-hot编码:
# 对提取的日期特征进行One-hot编码
OneHotCol2 = ['registerDate_year', 'registerDate_month', 'registerDate_dayofweek', 'tradeTime_year', 'tradeTime_month',
'tradeTime_dayofweek', 'licenseDate_year', 'licenseDate_month', 'licenseDate_dayofweek', 'D_12_year',
'D_12_month']
OneHotCode2 = One_Hot(OneHotCol2)
Combine = pd.concat([Combine, OneHotCode2], axis=1)
3.构建新特征
通过上述的几个日期信息变量,我们可以考虑构建一些有价值的新的特征。在本题中,我构建的特征是汽车使用天数,汽车注册日期距今天数,汽车上线日期距今天数。
# 构建特征:汽车使用天数
Combine['used_time1'] = (pd.to_datetime(Combine['tradeTime'], format='%Y%m%d', errors='coerce') -
pd.to_datetime(Combine['registerDate'], format='%Y%m%d', errors='coerce')).dt.days
# 构建特征:汽车注册日期距今天数
Combine['used_time2'] = (
pd.datetime.now() - pd.to_datetime(Combine['registerDate'], format='%Y%m%d', errors='coerce')).dt.days
# 构建特征:汽车上线日期距今天数
Combine['used_time3'] = (pd.datetime.now() - pd.to_datetime(Combine['tradeTime'], format='%Y%m%d', errors='coerce')).dt.days
4.数据分桶
编写日期分桶函数函数,并对'used_time1', 'used_time2', 'used_time3'进行分桶:
# 数据分桶函数
def cut_group(df, cols, num_bins=50):
for col in cols:
all_range = int(df[col].max() - df[col].min())
# ceil():返回一个数的上取整数;floor():返回一个数的下舍整数
bin = [np.ceil(df[col].min() - 1) + np.floor(i * all_range / num_bins) for i in range(num_bins + 2)]
# bin是一个列表,区间两端的选取就是跟据bin里的数据决定。如第一个区间就是[bin[0], bin[1]]
df[col + '_bin'] = pd.cut(df[col], bin, labels=False)
return df
# 对汽车使用天数,汽车注册日期距今天数 ,汽车上线日期距今天数进行数据分桶
CutCol = ['used_time1', 'used_time2', 'used_time3']
Combine = cut_group(Combine, CutCol, 50)
数据分桶是一种将多个连续值分组为较少数量的“桶”的方法,也就是将连续的多个值分成区间的方法,可以减小数据量。
5.特征扩充
将匿名特征之间相加,非匿名特征与匿名特征相乘,通过特征扩充进而获得更多信息:
list1 = [1, 2, 4, 5, 7, 8, 9, 10, 12, 13]
for i in ['D_' + str(m) for m in list1]:
for j in ['D_' + str(n) for n in list1]:
Combine[str(i) + '+' + str(j)] = Combine[i] + Combine[j]
for i in ['brand', 'serial', 'model', 'mileage', 'color', 'cityId', 'carCode', 'transferCount', 'seatings', 'country',
'maketype', 'modelyear', 'displacement', 'gearbox', 'oiltype', 'newprice', 'length', 'width', 'high']:
for j in ['D_' + str(n) for n in list1]:
Combine[str(i) + '*' + str(j)] = Combine[i] * Combine[j]
6.特征交叉
首先,我们分析一下匿名变量和非匿名变量与price的相关性:
AllCol = Combine.columns
Train = Combine.iloc[:len(Train), :][AllCol]
a = dict(Train.corr()['price']) # 各变量与price变量的相关性
asortlist = sorted(a.items(), key=lambda x: x[1], reverse=True) # 以字典的值为基准对字典的项进行排序
for i in asortlist:
print(i)
我将各变量按照其与price变量相关性的大小从高到低排列,如下图:
上图中,变量名后的数值越大,表示其与price的相关性越大。然后,我们挑选与price相关性较大的匿名变量和非匿名变量,让它们进行特征交叉,从而得到更复杂的非线性特征:
# 特征交叉函数
def cross_feature(df, fea_col, Nfea_col):
for i in tqdm(fea_col): # 遍历分类特征
for j in tqdm(Nfea_col): # 遍历数值特征
# 调用groupby()函数,以参数i分组,之后,用agg函数对数据做一些聚合操作(求最大值、最小值、中位数)
feat = df.groupby(i, as_index=False)[j].agg({
'{}_{}_max'.format(i, j): 'max', # 最大值
'{}_{}_min'.format(i, j): 'min', # 最小值
'{}_{}_median'.format(i, j): 'median', # 中位数
})
df = df.merge(feat, on=i, how='left')
return (df)
# 挑选与Price相关程度高的非匿名变量和匿名变量作特征交叉
Cross_fea = ['newprice', 'displacement', 'width', 'length', 'maketype', 'maketype_3', 'modelyear']
Cross_Nfea = ['D_1', 'D_10_3', 'D_7', 'D_7_5', 'D_10', 'D_4', 'D_12']
Combine = cross_feature(Combine, Cross_fea, Cross_Nfea)
进行完上述操作后,为便于后面的操作,先将训练集和测试集进行还原:
# 还原训练集和测试集
InputCol = Combine.columns.drop('price')
XTrain = Combine.iloc[:len(Train), :][InputCol]
YTrain = Train['price']
XTest = Combine.iloc[len(Train):, :][InputCol]
print("XTrain shape: ", XTrain.shape)
print("XTestshape: ", XTest.shape)
7.平均数编码
对于高基数特征,可以使用平均数编码, 有监督地确定最适合这个定性特征的编码方式。高基数特征,简单地说就是一个特征有很多个取值。对于这些不适合使用One-hot编码的特征,有人提出了平均数编码这一概念。如果想详细了解平均数编码的话可以看一下这篇博客,我就是照搬这个大佬写的平均数编码的代码:
平均数编码:针对高基数定性特征(类别特征)的数据预处理/特征工程https://blog.csdn.net/juzexia/article/details/78581462?spm=1001.2014.3001.5506 平均数编码实现代码:
class MeanEncoder:
def __init__(self, categorical_features, n_splits=10, target_type='classification', prior_weight_func=None):
self.categorical_features = categorical_features
self.n_splits = n_splits
self.learned_stats = {}
if target_type == 'classification':
self.target_type = target_type
self.target_values = []
else:
self.target_type = 'regression'
self.target_values = None
if isinstance(prior_weight_func, dict):
self.prior_weight_func = eval('lambda x: 1 / (1 + np.exp((x - k) / f))', dict(prior_weight_func, np=np))
elif callable(prior_weight_func):
self.prior_weight_func = prior_weight_func
else:
self.prior_weight_func = lambda x: 1 / (1 + np.exp((x - 2) / 1))
@staticmethod
def mean_encode_subroutine(X_train, y_train, X_test, variable, target, prior_weight_func):
X_train = X_train[[variable]].copy()
X_test = X_test[[variable]].copy()
if target is not None:
nf_name = '{}_pred_{}'.format(variable, target)
X_train['pred_temp'] = (y_train == target).astype(int) # classification
else:
nf_name = '{}_pred'.format(variable)
X_train['pred_temp'] = y_train # regression
prior = X_train['pred_temp'].mean()
col_avg_y = X_train.groupby(variable)['pred_temp'].agg(['mean', 'size']).rename(
columns={'mean': 'mean', 'size': 'beta'})
col_avg_y['beta'] = prior_weight_func(col_avg_y['beta'])
col_avg_y[nf_name] = col_avg_y['beta'] * prior + (1 - col_avg_y['beta']) * col_avg_y['mean']
col_avg_y.drop(['beta', 'mean'], axis=1, inplace=True)
nf_train = X_train.join(col_avg_y, on=variable)[nf_name].values
nf_test = X_test.join(col_avg_y, on=variable).fillna(prior, inplace=False)[nf_name].values
return nf_train, nf_test, prior, col_avg_y
def fit_transform(self, X, y):
X_new = X.copy()
if self.target_type == 'classification':
skf = StratifiedKFold(self.n_splits)
else:
skf = KFold(self.n_splits)
if self.target_type == 'classification':
self.target_values = sorted(set(y))
self.learned_stats = {'{}_pred_{}'.format(variable, target): [] for variable, target in
product(self.categorical_features, self.target_values)}
for variable, target in product(self.categorical_features, self.target_values):
nf_name = '{}_pred_{}'.format(variable, target)
X_new.loc[:, nf_name] = np.nan
for large_ind, small_ind in skf.split(y, y):
nf_large, nf_small, prior, col_avg_y = MeanEncoder.mean_encode_subroutine(
X_new.iloc[large_ind], y.iloc[large_ind], X_new.iloc[small_ind], variable, target,
self.prior_weight_func)
X_new.iloc[small_ind, -1] = nf_small
self.learned_stats[nf_name].append((prior, col_avg_y))
else:
self.learned_stats = {'{}_pred'.format(variable): [] for variable in self.categorical_features}
for variable in self.categorical_features:
nf_name = '{}_pred'.format(variable)
X_new.loc[:, nf_name] = np.nan
for large_ind, small_ind in skf.split(y, y):
nf_large, nf_small, prior, col_avg_y = MeanEncoder.mean_encode_subroutine(
X_new.iloc[large_ind], y.iloc[large_ind], X_new.iloc[small_ind], variable, None,
self.prior_weight_func)
X_new.iloc[small_ind, -1] = nf_small
self.learned_stats[nf_name].append((prior, col_avg_y))
return X_new
def transform(self, X):
X_new = X.copy()
if self.target_type == 'classification':
for variable, target in product(self.categorical_features, self.target_values):
nf_name = '{}_pred_{}'.format(variable, target)
X_new[nf_name] = 0
for prior, col_avg_y in self.learned_stats[nf_name]:
X_new[nf_name] += X_new[[variable]].join(col_avg_y, on=variable).fillna(prior, inplace=False)[
nf_name]
X_new[nf_name] /= self.n_splits
else:
for variable in self.categorical_features:
nf_name = '{}_pred'.format(variable)
X_new[nf_name] = 0
for prior, col_avg_y in self.learned_stats[nf_name]:
X_new[nf_name] += X_new[[variable]].join(col_avg_y, on=variable).fillna(prior, inplace=False)[
nf_name]
X_new[nf_name] /= self.n_splits
return X_new
之后,对部分特征进行平均数编码:
MeanEncol = ['model', 'brand', 'registerDate', 'tradeTime']
# 如果是回归场景,那么target_type='regression';如果是分类场景,那么target_type='classification'
MeanFit = MeanEncoder(MeanEncol, target_type='regression')
XTrain = MeanFit.fit_transform(XTrain, YTrain)
XTest = MeanFit.transform(XTest)
8.目标编码
目标编码是基于特征和目标值之间的对应关系的一种编码,下图是K折目标编码,K折目标编码将要编码的样本分成K份,每其中一份中的样本的目标编码,使用的是另外K-1份数据中相同类别的那些样本的对应属性,如最大最小值等,比普通的目标编码效果更好。
# K折目标编码,
# 回归场景中,对目标进行编码的常用方式:最小值、最大值、中位数、均值、求和、标准差、偏度、峰度、中位数绝对偏差
XTrain['price'] = Train['price']
EncCol = []
StatDefaultDict = {
'max': XTrain['price'].max(),
'min': XTrain['price'].min(),
'median': XTrain['price'].median(),
'mean': XTrain['price'].mean(),
'sum': XTrain['price'].sum(),
'std': XTrain['price'].std(),
'skew': XTrain['price'].skew(),
'kurt': XTrain['price'].kurt(),
'mad': XTrain['price'].mad()
}
# 采用最大值、最小值、均值对目标特征price分别进行编码
EncStat = ['max', 'min', 'mean']
# 分为10折
KF = KFold(n_splits=10, shuffle=True, random_state=2022)
for f in tqdm(['serial', 'brand', 'registerDate_year', 'tradeTime_year', 'mileage', 'model']):
EncDict = {}
for stat in EncStat:
EncDict['{}_target_{}'.format(f, stat)] = stat
XTrain['{}_target_{}'.format(f, stat)] = 0
XTest['{}_target_{}'.format(f, stat)] = 0
EncCol.append('{}_target_{}'.format(f, stat))
for i, (TrnIndex, ValIndex) in enumerate(KF.split(XTrain, YTrain)):
TrnX, ValX = XTrain.iloc[TrnIndex].reset_index(drop=True), XTrain.iloc[ValIndex].reset_index(drop=True)
EncDF = TrnX.groupby(f, as_index=False)['price'].agg(EncDict)
ValX = ValX[[f]].merge(EncDF, on=f, how='left')
TestX = XTest[[f]].merge(EncDF, on=f, how='left')
for stat in EncStat:
ValX['{}_target_{}'.format(f, stat)] = ValX['{}_target_{}'.format(f, stat)].fillna(StatDefaultDict[stat])
TestX['{}_target_{}'.format(f, stat)] = TestX['{}_target_{}'.format(f, stat)].fillna(StatDefaultDict[stat])
XTrain.loc[ValIndex, '{}_target_{}'.format(f, stat)] = ValX['{}_target_{}'.format(f, stat)].values
XTest['{}_target_{}'.format(f, stat)] += TestX['{}_target_{}'.format(f, stat)].values / KF.n_splits
上述过程结束后,我们查看一下现在数据的维度:
print("XTrain shape: ", XTrain.shape)
print("XTest shape: ", XTest.shape)
由上图可以看到,现在的特征个数已经达到了666个,但如果把这些全部喂给模型的话,其中的一些无用特征可能会影响我们的模型训练,所以我们就需要降维,最大程度地保留主要特征分量,筛除噪音。
本代码中,我使用的是PCA降维。关于PCA降维的步骤,我上一篇文章有说,所以这次我说的会简单一点。
首先,为了需要确保特征度量的比例尺度一致,我们使用极差法进行特征的归一化。
归一化公式如下:
上式中, 为样本数据最大值, 为样本数据最小值。
基于上述分析,我们先调用 sklearn 库中 preprocessing 模块中的 MinMaxScalar()函数对特征进行归一化处理,接着调用sklearn库中decomposition模块中的PCA算法包对数据进行降维。其步骤为:
1.去均值:
其中,为标准化后的矩阵, 为矩阵A的均值。
2.求标准化数据集的协方差矩阵:
3.计算协方差矩阵的特征值和特征向量。设数λ和n维非0列向量x满足下式:
则λ为C的特征值,x称为C的对应于特征值λ的特征向量。C为数据集的协方差矩阵。
4.保留最重要的前k个特征。k即为你想降维到的维数。
5.找到这k个特征值对应的特征向量
6.将标准化数据集乘以这k个特征向量,得到降维后的结果
上式中,表示上述的k个特征值分别对应的特征向量组成的矩阵。
上述完整过程代码实现的话如下:
# 归一化(极差法)
Scaler = MinMaxScaler()
Scaler.fit(pd.concat([XTrain, XTest]).values)
CombineScaler = Scaler.transform(pd.concat([XTrain, XTest]).values)
print('CombineScaler shape: ', CombineScaler.shape)
# 调用sklearn库中decomposition模块中的PCA算法包对数据进行降维操作
# PCA降维
PCA = decomposition.PCA(n_components=550)
CombinePCA = PCA.fit_transform(CombineScaler)
XTrainPCA = CombinePCA[:len(XTrain)]
XTestPCA = CombinePCA[len(XTrain):]
YTrain = Train['price'].values
print('CombinePCA shape: ', CombinePCA.shape)
本次神经网络模型,我用的是知乎大佬定义的一个神经网络,使用效果还可以。
def NN_model(input_dim):
# 参数随机初始化
init = keras.initializers.glorot_uniform(seed=1)
model = keras.models.Sequential()
model.add(Dense(units=300, use_bias=True, input_dim=input_dim, kernel_initializer=init, activation='softplus'))
model.add(Dense(units=300, use_bias=True, kernel_initializer=init, activation='softplus')) # ReLU
model.add(Dense(units=64, use_bias=True, kernel_initializer=init, activation='softplus'))
model.add(Dense(units=32, use_bias=True, kernel_initializer=init, activation='softplus'))
model.add(Dense(units=8, use_bias=True, kernel_initializer=init, activation='softplus'))
model.add(Dense(units=1))
return model
class Metric(Callback):
def __init__(self, model, callbacks, Combine):
super().__init__()
self.model = model
self.callbacks = callbacks
self.Combine = Combine
def on_train_begin(self, logs=None):
for callback in self.callbacks:
callback.on_train_begin(logs)
def on_train_end(self, logs=None):
for callback in self.callbacks:
callback.on_train_end(logs)
def on_epoch_end(self, batch, logs=None):
X_train, y_train = self.Combine[0][0], self.Combine[0][1]
y_pred3 = self.model.predict(X_train)
y_pred = np.zeros((len(y_pred3),))
y_true = np.zeros((len(y_pred3),))
for i in range(len(y_pred3)):
y_pred[i] = y_pred3[i]
for i in range(len(y_pred3)):
y_true[i] = y_train[i]
trn_s = metrics.mean_absolute_error(y_true, y_pred)
logs['trn_score'] = trn_s
X_val, y_val = self.Combine[1][0], self.Combine[1][1]
y_pred3 = self.model.predict(X_val)
y_pred = np.zeros((len(y_pred3),))
y_true = np.zeros((len(y_pred3),))
for i in range(len(y_pred3)):
y_pred[i] = y_pred3[i]
for i in range(len(y_pred3)):
y_true[i] = y_val[i]
val_s = metrics.mean_absolute_error(y_true, y_pred)
logs['val_score'] = val_s
print('trn_score', trn_s, 'val_score', val_s)
for callback in self.callbacks:
callback.on_epoch_end(batch, logs)
神经网络模型的结构图我画了一下,大概如下所示:
因为神经元比较多,所以权值w并未在图中标出。下面我以自己的理解简单介绍一下这个神经网络:
在该神经网络中,人造神经元可以接受n个实数值输入,他们构成输入向量。输入的下一级是一个线性单元。线性单元的输出为:
为偏置项,可以理解为y轴上的截距,如果没有偏置项的话,那么对于不过原点的函数,神经元将永远无法对其拟合。为第i个输入对应的权值。为了表达方便,我们令=bias,并为输入向量添加一个值为1的特征,此时的输出为:
可以看出是对神经元的电位水平的模拟,而则是阈值。线性单元的单值输出会送给下一级激活单元(激活函数),我们选用的激活函数为softplus函数,该函数的数学表达式为:
所以最终得到神经元的输出函数为:
代入得:
我们选用的损失函数是MAE(平均绝对误差),其误差计算如下:
其中,m表示样本点个数,表示第i个真实值,表示第i个预测值。在神经网络中,损失函数是为了计算误差,然后根据误差对神经网络训练中的权值进行更新。
动态调整学习率:
def scheduler(epoch):
# 每隔20个epoch,学习率减小为原来的二分之一
if epoch % 20 == 0 and epoch != 0:
lr = K.get_value(model.optimizer.lr)
K.set_value(model.optimizer.lr, lr * 0.5)
print("lr changed to {}".format(lr * 0.5))
return K.get_value(model.optimizer.lr)
reduce_lr = LearningRateScheduler(scheduler)
模型训练:
N = 10 # 分10折交叉验证
kfold = KFold(n_splits=N, shuffle=True)
BSize = 2000
MaxEpochs = 140
RinPred = np.zeros((len(XTrainPCA),))
for fold, (trn_idx, val_idx) in enumerate(kfold.split(XTrainPCA, YTrain)):
print('fold:', fold+1)
X_train, y_train = XTrainPCA[trn_idx], YTrain[trn_idx]
X_val, y_val = XTrainPCA[val_idx], YTrain[val_idx]
model = NN_model(X_train.shape[1])
# 学习率初始设为0.01
simple_adam = Adam(lr=0.01)
model.compile(loss='mae', optimizer=simple_adam, metrics=['mae'])
es = EarlyStopping(monitor='val_score', patience=10, verbose=1, mode='min', restore_best_weights=True, )
es.set_model(model)
metric = Metric(model, [es], [(X_train, y_train), (X_val, y_val)])
# batch_size:每一次权重更新需要batch_size个数据进行运算得到损失函数,每运算batch_size个数据相当于一次迭代,每次进行迭代将会更新参数的权重。
# epochs:被定义为向前和向后传播中所有批次的单次训练迭代。简单说,epochs指的就是训练过程中数据将被“轮”多少次
# 假设训练集有1000个样本,batchsize=10,那么训练完整个样本集需要: 100次iteration,1次epoch
model.fit(X_train, y_train, batch_size=BSize, epochs=MaxEpochs,
validation_data=(X_val, y_val),
callbacks=[reduce_lr], shuffle=True, verbose=1)
y_pred3 = model.predict(X_val)
y_pred = np.zeros((len(y_pred3),))
for i in range(len(y_pred3)):
y_pred[i] = y_pred3[i]
RinPred[val_idx] = y_pred
np.set_printoptions(suppress=True) # 不以科学计数法输出
# 训练集真实值
# print(np.around(YTrain[val_idx], 2))
# 训练集预测值
# print(np.around(y_pred, 2))
# 输出data2中二手车价格的预测值
print(np.around(model.predict(XTestPCA), 2))
print(Evaluate(YTrain[val_idx], y_pred))
模型训练中,优化器我们使用的是Adam。神经网络网络会根据损失函数,利用Adam优化器,使用反向传播算法来更新网络参数权重,以此训练网络模型,对神经网络进行优化。Adam优化器进行权值更新的公式较为复杂,所以我就不再列出,想了解的话在网上搜索一下就能够找到。
跟据题目给出的模型评价标准:
定义模型评价函数:
# 评价模型
def Evaluate(y_tre, y_pre):
# y_tre:真实值;y_pre:预测值
m = len(y_tre)
count1 = 0
Ape = []
for i in range(0, m):
Ape.append(np.abs(y_pre[i] - y_tre[i]) / y_tre[i])
Mape = sum(Ape) / m
for i in Ape:
if i <= 0.05:
count1 += 1
Accuracy = count1 / m
print('Mape:', Mape)
print('Accuracy', Accuracy)
print('score', 0.2 * (1 - Mape) + 0.8 * Accuracy)
运行代码,跟据评价函数的评判,结果较好的一次为:
为了更好地展示模型的预测结果,我随机抽取了训练集中的部分数据,将它们的真实值和预测值作比较,可视化后如下图所示:
可以看出,模型的预测结果整体还是不错的。
我也是刚开始接触机器学习,如果有错误的地方,希望能够指正。