李宏毅机器学习--课后作业HW_1

看了李宏毅老师讲的机器学习课程,真的是受益匪浅,老师讲课非常有意思,不是空洞的讲数学公式,以及抽象的理论,而是通过在课堂上加入游戏元素来引导大家学习,我个人觉得这种方式是很好的,而且原理讲的也很清楚,适合小白学习,这里有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,数据下载链接,有原始数据以及结果

李宏毅机器学习--课后作业HW_1_第1张图片

打开train.csv文件,如下:

 李宏毅机器学习--课后作业HW_1_第2张图片

第一行是说明,分别是时间,地点,物质的名称,以及0-23时他们的浓度。
注意RAINFALL参数的值是NR(No Rain),表示没有下雨,后面可以将它的值改为0便于处理。 

 打开test.csv文件,如下:

李宏毅机器学习--课后作业HW_1_第3张图片

1.解读一下题目:

Dataset里面我们要用的两个csv文件:数据集(train.csv)和测试集(test.csv)。
大概的内容是数据集中记录了12个月的前20天每天24个小时的18个物质浓度的资料,然后测试集就是剩余的数据中再取的。从上述我们可以得到以下结论:

  •  模型的输入是前9个小时的所有观测数据,即9*18的参数值,
  •  模型输出是一个值表示预测的第10个小时的PM2.5含量。
     所以最适合的模型是Regression model(回归模型)
  • 根据作业要求可知,需要用到连续9个时间点的气象观测数据,来预测第10个时间点的PM2.5含量。针对每一天来说,其包含的信息维度为(18,24)(18项指标,24个时间节点)。可以将0到8时的数据截取出来,形成一个维度为(18,9)的数据帧,作为训练数据,将9时的PM2.5含量取出来,作为该训练数据对应的label;同理可取1到9时的数据作为训练用的数据帧,10时的PM2.5含量作为label......以此分割,可将每天的信息分割为15个shape为(18,9)的数据帧和与之对应的15个label,来自博主秋沐霖。我们可以用滑动窗口的思想,需要将一个月的第一天到第二十天横向排序,取大小为9的窗口,从第一天的第0时一直可以划到第20天的第14时,这样每个月的数据量就会多9个,这样积累的话使整个数据量就会更多。

图来自博主秋沐霖。李宏毅机器学习--课后作业HW_1_第4张图片

李宏毅机器学习--课后作业HW_1_第5张图片

 2.数据预处理:

我们把题目搞清楚以后就开始对要训练的数据(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)
# 可以每一步都打印出结果,看到数据的变化

接下来,将每个月的数据按天数横着排放在一起。

李宏毅机器学习--课后作业HW_1_第6张图片

李宏毅机器学习--课后作业HW_1_第7张图片

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]

李宏毅机器学习--课后作业HW_1_第8张图片

上述所有工作做完之后,X就是包含所有数据的数组,Y就是所有数据的结果,但此时还不可以进行训练,还需要两个步骤才可以。

3.Normalize(标准化)与训练集分类

数据的标准化(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))

 运行以后结果如下:

李宏毅机器学习--课后作业HW_1_第9张图片

 可以看到训练的数据是:4521,验证的数据是:1131。

4.训练模型

对数据做好处理以后,我们开始训练我们的模型,使用的是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),即下图所示的公式:

李宏毅机器学习--课后作业HW_1_第10张图片

至于学习率,最简单的办法就是固定学习率,每次迭代的学习率都相同,但这样效果也会很差,良好的学习率应随迭代次数依次减少,所以我们使用自适应学习率的adagrad算法(想了解更清楚的可以去看一下李宏毅老师的课程,讲的很详细),即每次学习率都等于其除以之前所有的梯度平方和再开根号,即下图所示的公式: 

​​​​​​​李宏毅机器学习--课后作业HW_1_第11张图片 

gradient就是一个对Loss函数进行每个参数的偏微分而得到的矩阵。 

5.载入验证集进行验证

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进行训练,并通过x_validation和y_validation进行验证,结果是
    训练集中最后一次迭代的loss为5.68 测试集中loss为5.42
  • 李宏毅机器学习--课后作业HW_1_第12张图片

但上述情况是因为x_validation本身包括在训练集中,所以loss比下面方法更低在意料之中

  •  使用x_train_set进行训练,并通过x_validation和y_validation进行验证,结果是
    训练集中最后一次迭代的loss为5.72 测试集中loss为5.66
  • 李宏毅机器学习--课后作业HW_1_第13张图片

这样看来,我们的模型还算可以,所以就可以进行最后一步预测test data了
还需要补充的是,loss function一定和训练的loss function一致才可以通过比较它俩loss大小来评估模型好坏。然后就是如果使用x_train_set进行训练时,将训练步骤的X全部换成x_train_set,并且在”/471/12”的时候除的是x_validation的大小,即/4521

6.预测testdata得到预测结果

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)

李宏毅机器学习--课后作业HW_1_第14张图片

以上所有代码都是在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)

7.优化

本文用的是均方根误差和Adagrad算法,还有很多更好的其他的算法,还可以加入正则项,参考博主秋沐霖,大家可以多去试试。

你可能感兴趣的:(李宏毅机器学习,python,机器学习)