看了李宏毅老师讲的机器学习课程,真的是受益匪浅,老师讲课非常有意思,不是空洞的讲数学公式,以及抽象的理论,而是通过在课堂上加入游戏元素来引导大家学习,我个人觉得这种方式是很好的,而且原理讲的也很清楚,适合小白学习,这里有B站的视频链接,想学习的可以去看一下【李宏毅《机器学习》-哔哩哔哩】,可以配着学习笔记学(李宏毅机器学习笔记,来自于Datawhale的学习笔记,这个平台也是国内一个很好的学习AI的开源平台,有兴趣的可以去关注下),本文借鉴了很多博主的博客,大部分内容来自于博主永缘客,并结合自己理解的,写一篇博客记录一下,水平有限,如果有任何错误或者问题,欢迎指正或者交流。
先看一下作业是什么。
作业描述
让机器预测到丰原站在下一个小时会观测到的PM2.5。举例来说,现在是2017-09-29 08:00:00 ,那么要预测2017-09-29 09:00:00丰原站的PM2.5值会是多少。
数据介绍: 本次作业使用豐原站的觀測記錄,分成 train set 跟 test set,train set 是豐原站每個月的前20天所有資料,test set則是從豐原站剩下的資料中取樣出來。 train.csv:每個月前20天每個小時的氣象資料(每小時有18種測資)。共12個月。 test.csv:從剩下的資料當中取樣出連續的10小時為一筆,前九小時的所有觀測數據當作feature,第十小時的PM2.5當作answer。一共取出240筆不重複的 test data,請根據feature預測這240筆的PM2.5。
Sample_code:https://github.com/datawhalechina/leeml-notes/tree/master/docs/Homework/HW_1,数据下载链接,有原始数据以及结果
打开train.csv文件,如下:
第一行是说明,分别是时间,地点,物质的名称,以及0-23时他们的浓度。
注意RAINFALL参数的值是NR(No Rain),表示没有下雨,后面可以将它的值改为0便于处理。
打开test.csv文件,如下:
Dataset里面我们要用的两个csv文件:数据集(train.csv)和测试集(test.csv)。
大概的内容是数据集中记录了12个月的前20天每天24个小时的18个物质浓度的资料,然后测试集就是剩余的数据中再取的。从上述我们可以得到以下结论:
我们把题目搞清楚以后就开始对要训练的数据(train.csv)进行预处理,因为在python中数据是通过矩阵来保存的,所以我们第一步就是删减掉不需要的行与列,然后将其保存到矩阵中。
# 引入必要的包
import pandas as pd
import numpy as np
# 读取数据保存到data中,路径根据你保存的train.csv位置而有变化
data = pd.read_csv('./dataset/train.csv', encoding='utf-8')
# print(data)
# 行保留所有,列从第三列开始往后才保留,这样去除了数据中的时间、地点、参数等信息
data = data.iloc[:, 3:]
# print(data)
# 将所有NR的值全部置为0方便之后处理
data[data == 'NR'] = 0
# print(data)
# 将data的所有数据转换为二维数据并用raw_data来保存
raw_data = data.to_numpy()
# print(raw_data)
# 可以每一步都打印出结果,看到数据的变化
接下来,将每个月的数据按天数横着排放在一起。
month_data = {}
# month 从0-11 共12个月
# 返回一个18行480列的数组,保存一个月的data(一月20天,一天24小时)
# day从0-19 共20天
for month in range(12):
sample = np.empty([18, 480])
for day in range(20):
# raw的行每次取18行,列取全部列。送到sample中(sample是18行480列)
# 行给全部行,列只给24列,然后列往后增加
sample[:, day * 24: (day + 1) * 24] = raw_data[18 * (20 * month + day): 18 * (20 * month + day + 1),:]
month_data[month] = sample
这样每个月的数据就是18行480(24*20)列,一共12个月的数据
然后继续对上述数据进行提取:
# 每月共480个小时,每9个小时一个数据(480列最后一列不可以计入,如果取到最后一列那么最后一个数据
# 便没有了结果{需要9个小时的输入和第10个小时的第10行作为结果}),480-1-9+1=471。
# 12个月共12*471个数据按行排列,每一行一个数据;数据每小时有18个特征,而每个数据9个小时,共18*9列
x = np.empty([12 * 471, 18 * 9], dtype=float)
# 12个月共12*471个数据,每个数据对应一个结果,即第10小时的PM2.5浓度
y = np.empty([12 * 471, 1], dtype=float)
for month in range(12): # month 0-11
for day in range(20): # day 0-19
for hour in range(24): # hour 0-23
if day == 19 and hour > 14: # 第20天的第23时
continue
# 取对应month:行都要取,列取9个,依次进行,最后将整个数据reshape成一行数据
# (列数无所谓)。然后赋给x,x内的坐标只是为了保证其从0-471*12
# vector dim:18*9
x[month * 471 + day * 24 + hour, :] = month_data[month][:, day * 24 + hour: day * 24 + hour + 9].reshape(1, -1)
# value,结果对应的行数一直是9行(即第10行PM2.5)然后列数随着取得数据依次往后进行
y[month * 471 + day * 24 + hour, 0] = month_data[month][9, day * 24 + hour + 9]
上述所有工作做完之后,X就是包含所有数据的数组,Y就是所有数据的结果,但此时还不可以进行训练,还需要两个步骤才可以。
数据的标准化(normalization)是将数据按比例缩放,使之落入一个小的特定区间。
在某些比较和评价的指标处理中经常会用到,去除数据的单位限制,将其转化为无量纲的纯数值,
便于不同单位或量级的指标能够进行比较和加权。
最常见的标准化方法就是Z标准化,也是SPSS中最为常用的标准化方法,spss默认的标准化方法就是z-score标准化。
也叫标准差标准化,这种方法给予原始数据的均值(mean)和标准差(standard deviation)进行数据的标准化。
经过处理的数据符合标准正态分布,即均值为0,标准差为1,注意,一般来说z-score不是归一化,而是标准化,归一化只是标准化的一种[lz]。
其转化函数为:
x* = (x - μ ) / σ
其中μ为所有样本数据的均值,σ为所有样本数据的标准差。
详细的可以参考这篇博客数据预处理-归一化/数据转换。
mean_x = np.mean(x, axis=0) # 18 * 9 求均值,axis = 0表示对各列求值,返回 1* 列数 的矩阵
std_x = np.std(x, axis=0) # 18 * 9 求标准差,axis = 0表示对各列求值,返回 1* 列数 的矩阵
for i in range(len(x)): # 12 * 471
for j in range(len(x[0])): # 18 * 9
if std_x[j] != 0:
x[i][j] = (x[i][j] - mean_x[j]) / std_x[j]
接下来是将训练数据按8:2拆成训练数据和验证数据。这样的好处是因为最终只给我们test data的输入而没有给我们输出,所以我们无法定量我们模型的好坏,而使用验证数据可以简单验证我们模型的好坏,让我们自己心里有数。
# 将训练数据拆成训练数据:验证数据=8:2,这样用来验证
import math
x_train_set = x[: math.floor(len(x) * 0.8), :]
y_train_set = y[: math.floor(len(y) * 0.8), :]
x_validation = x[math.floor(len(x) * 0.8):, :]
y_validation = y[math.floor(len(y) * 0.8):, :]
print(x_train_set)
print(y_train_set)
print(x_validation)
print(y_validation)
print(len(x_train_set))
print(len(y_train_set))
print(len(x_validation))
print(len(y_validation))
运行以后结果如下:
可以看到训练的数据是:4521,验证的数据是:1131。
对数据做好处理以后,我们开始训练我们的模型,使用的是Loss Function(损失函数)和GD(Gradient Descent梯度下降)算法。
# 用来做参数vector的维数,加1是为了对bias好处理(还有个误差项)。即h(x)=w1x1+w2x2+''+wnxn+b
dim = 18 * 9 + 1
# 生成一个dim行1列的数组用来保存参数值
w = np.ones([dim, 1])
# np.ones来生成12*471行1列的全1数组,np.concatenate,axis=1
# 表示按列将两个数组拼接起来,即在x最前面新加一列内容,之前x是12*471行
# 18*9列的数组,新加一列之后变为12*471行18*9+1列的数组'''
x = np.concatenate((np.ones([12 * 471, 1]), x), axis=1).astype(float)
learning_rate = 100 # 学习率
iter_time = 10000 # 迭代次数
# 生成dim行即163行1列的数组,用来使用adagrad算法更新学习率
adagrad = np.zeros([dim, 1])
# 因为新的学习率是learning_rate/sqrt(sum_of_pre_grads**2),
# 而adagrad=sum_of_grads**2,所以处在分母上而迭代时adagrad可能为0,
# 所以加上一个极小数,使其不除0
eps = 0.0000000001
for t in range(iter_time):
# rmse loss函数是从0-n的(X*W-Y)**2之和/(471*12)再开根号,
# 即使用均方根误差(root mean square error),具体可看公式,
# /471/12即/N(次数)'''
loss = np.sqrt(np.sum(np.power(np.dot(x, w) - y, 2)) / 471 / 12)
if (t % 100 == 0): # 每一百次迭代就输出其损失
print(str(t) + ":" + str(loss))
# dim*1 x.transpose即x的转置,后面是X*W-Y,即2*(x的转置*(X*W-Y))是梯度,
# 具体可由h(x)求偏微分获得.最后生成1行18*9+1列的数组。转置后的X,其每一行
# 是一个参数,与h(x)-y的值相乘之后是参数W0的修正值,同理可得W0-Wn的修正值
# 保存到1行18*9+1列的数组中,即gradient
gradient = 2 * np.dot(x.transpose(), np.dot(x, w) - y)
# adagrad用于保存前面使用到的所有gradient的平方,进而在更新时用于调整学习率
adagrad += gradient ** 2
w = w - learning_rate * gradient / np.sqrt(adagrad + eps) # 更新权重
np.save('weight.npy', w) # 将参数保存下来
注意的是,loss function我们选择的是均方根误差(Root Mean Square Error),即下图所示的公式:
至于学习率,最简单的办法就是固定学习率,每次迭代的学习率都相同,但这样效果也会很差,良好的学习率应随迭代次数依次减少,所以我们使用自适应学习率的adagrad算法(想了解更清楚的可以去看一下李宏毅老师的课程,讲的很详细),即每次学习率都等于其除以之前所有的梯度平方和再开根号,即下图所示的公式:
w = np.load('weight.npy')
# 使用x_validation和y_validation来计算模型的准确率,因为X已经normalize了,
# 所以不需要再来一遍,只需在x_validation上添加新的一列作为bias的乘数即可
x_validation = np.concatenate((np.ones([1131, 1]), x_validation), axis=1).astype(float)
ans_y = np.dot(x_validation, w)
loss = np.sqrt(np.sum(np.power(ans_y - y_validation, 2)) / 1131)
print(loss)
细心的你肯定注意到了,我在第四步进行训练使用的训练集是整个X,而没使用我分开好的x_train_set,这是因为使用了整个X训练之后就直接可以预测testdata中的值并输出了,而使用x_train_set进行训练再使用x_validation和y_validation进行验证是为了预估模型的好坏。但我还是使用了两种训练集分别进行训练,并都用x_validation和y_validation进行验证,结果如下:
但上述情况是因为x_validation本身包括在训练集中,所以loss比下面方法更低在意料之中
这样看来,我们的模型还算可以,所以就可以进行最后一步预测test data了
还需要补充的是,loss function一定和训练的loss function一致才可以通过比较它俩loss大小来评估模型好坏。然后就是如果使用x_train_set进行训练时,将训练步骤的X全部换成x_train_set,并且在”/471/12”的时候除的是x_validation的大小,即/4521
testdata = pd.read_csv('./dataset/test.csv', header=None, encoding='utf-8')
test_data = testdata.iloc[:, 2:] # 取csv文件中的全行数即第3列到结束的列数所包含的数据
test_data[test_data == 'NR'] = 0 # 将testdata中的NR替换为0
test_data = test_data.to_numpy() # 将其转换为数组
# 创建一个240行18*9列的空数列用于保存textdata的输入
test_x = np.empty([240, 18 * 9], dtype=float)
for i in range(240): # 共240个测试输入数据
test_x[i, :] = test_data[18 * i: 18 * (i + 1), :].reshape(1, -1)
# 下面是Normalize,且必须跟training data是同一种方法进行Normalize
for i in range(len(test_x)):
for j in range(len(test_x[0])):
if std_x[j] != 0:
test_x[i][j] = (test_x[i][j] - mean_x[j]) / std_x[j]
# 在test_x前面拼接一列全1数组,构成240行,163列数据
test_x = np.concatenate((np.ones([240, 1]), test_x), axis=1).astype(float)
# 进行预测
w = np.load('weight.npy')
ans_y = np.dot(test_x, w) # test data的预测值ans_y=test_x与W的积
# 将预测结果填入文件当中
import csv
with open('submit.csv', mode='w', newline='') as submit_file:
csv_writer = csv.writer(submit_file)
header = ['id', 'value']
print(header)
csv_writer.writerow(header)
for i in range(240):
row = ['id_' + str(i), ans_y[i][0]]
csv_writer.writerow(row)
print(row)
以上所有代码都是在Jupyter里面运行的,我自己都跑了一遍,没有问题。自己又在VS Code里面跑了也可以正常运行。
在VS Code里面跑的全部代码:
import pandas as pd
import numpy as np
import math
import csv
# 读取数据保存到data中,路径根据你保存的train.csv位置而有变化
data = pd.read_csv('~/GitHub/leeml-notes/docs/Homework/HW_1/dataset/train.csv', encoding='utf-8')
# print(data)
# 行保留所有,列从第三列开始往后才保留,这样去除了数据中的时间、地点、参数等信息
data = data.iloc[:, 3:]
# print(data)
# 将所有NR的值全部置为0方便之后处理
data[data == 'NR'] = 0
# print(data)
# 将data的所有数据转换为二维数据并用raw_data来保存
raw_data = data.to_numpy()
# print(raw_data)
# 可以每一步都打印出结果,看到数据的变化
month_data = {}
# month 从0-11 共12个月
# 返回一个18行480列的数组,保存一个月的data(一月20天,一天24小时)
# day从0-19 共20天
for month in range(12):
sample = np.empty([18, 480])
for day in range(20):
# raw的行每次取18行,列取全部列。送到sample中(sample是18行480列)
# 行给全部行,列只给24列,然后列往后增加
sample[:, day * 24: (day + 1) * 24] = raw_data[18 * (20 * month + day): 18 * (20 * month + day + 1),:]
month_data[month] = sample
# 一共480个小时,每9个小时一个数据(480列最后一列不可以计入,因为如果取到最后一行那么最后一个数据
# 便没有了结果{需要9个小时的输入和第10个小时的第10行作为结果}),480-1-9+1=471。
# 471*12个数据集按行排列,每一行一个数据;数据每小时有18个特征,而每个数据9个小时,共18*9列
x = np.empty([12 * 471, 18 * 9], dtype=float)
# 结果是471*12个数据,每个数据对应一个结果,即第10小时的PM2.5浓度
y = np.empty([12 * 471, 1], dtype=float)
for month in range(12): # month 0-11
for day in range(20): # day 0-19
for hour in range(24): # hour 0-23
if day == 19 and hour > 14: # 取到行18,列9的块后,就不可再取了
continue
# 取对应month:行都要取,列取9个,依次进行,最后将整个数据reshape成一行数据(列数无所谓)。然后赋给x,x内的坐标只是为了保证其从0-471*12
# vector dim:18*9
# value,结果对应的行数一直是第9列(即第10行PM2.5)然后列数随着取得数据依次往后进行
x[month * 471 + day * 24 + hour, :] = month_data[month][:, day * 24 + hour: day * 24 + hour + 9].reshape(1, -1)
y[month * 471 + day * 24 + hour, 0] = month_data[month][9, day * 24 + hour + 9]
mean_x = np.mean(x, axis=0) # 18 * 9 求均值,axis = 0表示对各列求均值,返回 1* 列数 的矩阵
std_x = np.std(x, axis=0) # 18 * 9 求标准差,axis = 0表示对各列求均值,返回 1* 列数 的矩阵
for i in range(len(x)): # 12 * 471
for j in range(len(x[0])): # 18 * 9
if std_x[j] != 0:
x[i][j] = (x[i][j] - mean_x[j]) / std_x[j]
# 将训练数据拆成训练数据:验证数据=8:2,这样用来验证
x_train_set = x[: math.floor(len(x) * 0.8), :]
y_train_set = y[: math.floor(len(y) * 0.8), :]
x_validation = x[math.floor(len(x) * 0.8):, :]
y_validation = y[math.floor(len(y) * 0.8):, :]
print(x_train_set)
print(y_train_set)
print(x_validation)
print(y_validation)
print(len(x_train_set))
print(len(y_train_set))
print(len(x_validation))
print(len(y_validation))
# 用来做参数vector的维数,加1是为了对bias好处理(还有个误差项)。即h(x)=w1x1+w2x2+''+wnxn+b
dim = 18 * 9 + 1
# 生成一个dim行1列的数组用来保存参数值,对比源码我这里改成了ones而不是zeros
w = np.ones([dim, 1])
'''np.ones来生成12*471行1列的全1数组,np.concatenate,axis=1
表示按列将两个数组拼接起来,即在x最前面新加一列内容,之前x是12*471行
18*9列的数组,新加一列之后变为12*471行18*9+1列的数组'''
x = np.concatenate((np.ones([12 * 471, 1]), x), axis=1).astype(float)
learning_rate = 100 # 学习率
iter_time = 10000 # 迭代次数
adagrad = np.zeros([dim, 1]) # 生成dim行即163行1列的数组,用来使用adagrad算法更新学习率
'''因为新的学习率是learning_rate/sqrt(sum_of_pre_grads**2),
而adagrad=sum_of_grads**2,所以处在分母上而迭代时adagrad可能为0,
所以加上一个极小数,使其不除0'''
eps = 0.0000000001
for t in range(iter_time):
'''rmse loss函数是从0-n的(X*W-Y)**2之和/(471*12)再开根号,
即使用均方根误差(root mean square error),具体可百度其公式,
/471/12即/N(次数)'''
loss = np.sqrt(np.sum(np.power(np.dot(x, w) - y,
2)) / 471 / 12)
if (t % 100 == 0): # 每一百次迭代就输出其损失
print(str(t) + ":" + str(loss))
'''dim*1 x.transpose即x的转置,后面是X*W-Y,即2*(x的转置*(X*W-Y))是梯度,
具体可由h(x)求偏微分获得.最后生成1行18*9+1列的数组。转置后的X,其每一行
是一个参数,与h(x)-y的值相乘之后是参数W0的修正值,同理可得W0-Wn的修正值
保存到1行18*9+1列的数组中,即gradient'''
gradient = 2 * np.dot(x.transpose(), np.dot(x,
w) - y)
# adagrad用于保存前面使用到的所有gradient的平方,进而在更新时用于调整学习率
adagrad += gradient ** 2
w = w - learning_rate * gradient / np.sqrt(adagrad + eps) # 更新权重
np.save('weight.npy', w) # 将参数保存下来
w = np.load('weight.npy')
# 使用x_validation和y_validation来计算模型的准确率,因为X已经normalize了,
# 所以不需要再来一遍,只需在x_validation上添加新的一列作为bias的乘数即可
x_validation = np.concatenate((np.ones([1131, 1]), x_validation), axis=1).astype(float)
ans_y = np.dot(x_validation, w)
loss = np.sqrt(np.sum(np.power(ans_y - y_validation, 2)) / 1131)
print('The Loss is :' + str(loss))
# 改成文件存在自己电脑的位置
testdata = pd.read_csv('~/GitHub/leeml-notes/docs/Homework/HW_1/dataset/test.csv', header=None, encoding='utf-8')
test_data = testdata.iloc[:, 2:] # 取csv文件中的全行数即第3列到结束的列数所包含的数据
test_data = test_data.replace(['NR'], [0.0]) # 将testdata中的NR替换为0
test_data = test_data.to_numpy() # 将其转换为数组
# 创建一个240行18*9列的空数列用于保存textdata的输入
test_x = np.empty([240, 18 * 9], dtype=float)
for i in range(240): # 共240个测试输入数据
test_x[i, :] = test_data[18 * i: 18 * (i + 1), :].reshape(1, -1)
# 下面是Normalize,且必须跟training data是同一种方法进行Normalize
for i in range(len(test_x)):
for j in range(len(test_x[0])):
if std_x[j] != 0:
test_x[i][j] = (test_x[i][j] - mean_x[j]) / std_x[j]
# 在test_x前面拼接一列全1数组,构成240行,163列数据
test_x = np.concatenate((np.ones([240, 1]), test_x), axis=1).astype(float)
# 进行预测
w = np.load('weight.npy')
ans_y = np.dot(test_x, w) # test data的预测值ans_y=test_x与W的积
# 将预测结果填入文件当中
with open('submit.csv', mode='w', newline='') as submit_file:
csv_writer = csv.writer(submit_file)
header = ['id', 'value']
print(header)
csv_writer.writerow(header)
for i in range(240):
row = ['id_' + str(i), ans_y[i][0]]
csv_writer.writerow(row)
print(row)
本文用的是均方根误差和Adagrad算法,还有很多更好的其他的算法,还可以加入正则项,参考博主秋沐霖,大家可以多去试试。