机器学习(五)——神经网络

一、什么是神经网络

如果前面了解了逻辑回归,其实神经网络比较容易理解。它就是一系列的类逻辑回归的组合。(注意我这里用的是类逻辑回归,因为它和逻辑回归还有一点点不同,稍后再解释)。借用吴恩达老师的一张图来理解:



这是一个两层神经网络(不算第一层输入层):其中,第一层为输入层,就是观测数据的各特征值,这里有3个特征;第二层为隐藏层(因为我们看不到它的输出);第三层为输出层,也就是我们经过神经网络计算后得到的结果。 第二层的每个神经元的结果,也是第三层输入。因此,神经网络就是一层一层的这种类逻辑回归所形成的网络。每一层的输出变量都是下 一层的输入变量。所以,其实神经网络的理论并不是很复杂的,对吧?
我们在逻辑回归中使用的逻辑函数,在神经网络中,有另外一个名字,叫激活函数。激活函数就是一些类似于sigmoid 的非线形函数,用于加入非线形因素。如果使用线形函数,将失去了神经网络多层的意义。因为多个线形函数的组合,最后还是一个线形函数,就没有必要使用多层神经网络传导来进行计算估计了。激活函数可以使用逻辑回归中的sigmoid函数,也可以使用tanh函数,还可以使用ReLU函数。一般来说,由于神经网络的参数很多,为了能够加快计算效率,建议选择ReLU函数(因为该函数计算导数非常方便)。sigmoid函数会将输出值限定在0到1之间,用在隐藏层会导致输入数据的的分布不是特别好,那么得出来的结果效果不太好。tanh函数只是将sigmod函数进行了向下拉升,将输出值限定在-1到1之间,这样输出值将会是以0为中心的分布,实现了数据的归一化。

二、理解反向传播

理解了神经网络之后,我们要面对的一个问题就是如何求出模型的参数。只有确定了参数,我们才能对新的数据来进行预测。
参数的确定一般分为如下几步:
1)先随机初始化所有的参数。(这里参数不能全部初始化为0或则相同的数,否则将失去了神经元的作用)
2)根据初始化的参数及初始的观测值来计算输出(正向传播);
3)根据我们计算出的值与实际观测值的结果进行对比,计算误差;
4)将误差反向传递到第一层
5)获取到第一层的误差后,根据误差来进行参数调整(可以使用梯度下降算法来调整参数)
6)重复步骤2~5,直到第3步中计算的结果和实际的结果之间的误差达到最小。

三、R代码实现

3.1 单神经元的口袋算法

先来一个简单的口袋感知器算法。一个感知器,其实就是最最最简单的神经网络。它只有一层,且没有激活函数。因此该感知器其实就是用梯度下降算法来做了一个线形回归的参数估计。
我们回顾一下线形回归的损失函数:
对于一个一元线性函数:,其对应的损失函数为:
梯度下降的目的就是要找到,能够最小化代价函数。
根据梯度下降的公式:,关键步骤是要求这个偏导数项。
而:
\begin{aligned} \frac {\partial } {\partial \theta_j }J(\theta)&=\frac {\partial } {\partial \theta_j }\frac{1}{2m}\sum_{i=1}^{m}(h_\theta(x^i)-y^i)^2\\ &= \frac {\partial } {\partial \theta_j }\frac{1}{2m}\sum_{i=1}^{m}(\theta_0 + \theta_1x^i-y^i)^2 \\ &= \frac{1}{m}\sum_{i=1}^{m}(h_\theta(x^i)-y^i)x^i \end{aligned}
注意,上面这个式子,我们也可以将截距项考虑进去,此时,只需要令 即可(即,)。
因此,对于线形回归来说,采用梯度下降来优化参数,我们只需要用我们通过模型估计出来的结果减去实际对应的结果,再乘以对应的x即可。
因此,对于口袋感知器,优化参数的整体思路如下:
1)随机初始化参数;
2)迭代所有观测值。每次迭代时,先使用正向传播获得预测值。然后使用预测值跟实际值进行比较。若一致,不更新参数,若不一致,根据上面的梯度下降算法,更新参数,同时记录不一致的次数,直到所有观测值全部迭代完毕。
3)若现有的不一致次数小于保存的不一致次数,则证明本轮参数优于之前的参数,将本轮参数保存到变量中;
4)重复2~3,直到总迭代次数超过给定的次数或则所有预测值都等于实际值为止。
代码如下:

# 初始化数据:
set.seed(4910341)
x1 = runif(200, 0, 10)  #随机生成200个在0到10之间均匀分布的数。(复习:随机正太分布函数是rnorm)
set.seed(2125151)
x2 = runif(200, 0, 10)
x = cbind(x1,x2)
y = sign(-0.89 + 2.07*x[,1] - 3.09*x[,2])

#输出决策。实现分类思想。如果小于0,判为-1,大于0,判为1
step_function <- function(x) {
   if (x < 0) -1 else 1
}

#实现口袋感知器算法
pocket_perceptron <- function(x, y, learning_rate, max_iterations) {
  nObs = nrow(x)
  nFeatures = ncol(x)
  w = rnorm(nFeatures+1,0,2) # 随机初始化权重w,均值为0,方差为2的正太随机分布。
  current_iteration = 0
  has_converged = F
  best_weights = w  #假设当前w就是最优的
  best_error = nObs #假设我们计算出来都数据跟观测数据相比都是错的,所以错误次数为行数
  while ((has_converged == F) & (current_iteration < max_iterations)) {
    has_converged = T # Assume we are done unless we misclassify an observation
    current_error = 0 # Keep track of misclassified observations
    for (i in 1:nObs) {
      xi = c(1,x[i,]) # 加上截距项x0。我们可认为x0=1
      yi = y[i]
      y_predicted = step_function(sum(w*xi))
      if (yi != y_predicted) {
        current_error = current_error + 1
        has_converged = F # We have at least one misclassified example
        # 调整w
        w = w + learning_rate*sign(yi-y_predicted)*xi  
      }
    }
    if (current_error < best_error) {
      best_error = current_error
      best_weights = w
    }
    current_iteration = current_iteration+1
  }
  model <- list("weights" = best_weights, "converged" = has_converged, "iterations" = current_iteration)
  model
}

pmodel = pocket_perceptron(x,y,0.1,1000)
pmodel

3.2 使用neuralnet 构建神经网络

neuralnet缺省的是用于线性回归。neuralnet包的改进在于提供了弹性反向传播算法和更多的激活函数形式
在R语言中对包括分类变量(factor)的数据建模时,一般会将其自动处理为虚拟变量或哑变量(dummy variable)。但有一些特殊的函数,如neuralnet包中的neuralnet函数就不会预处理。如果直接将原始数据扔进去,会出现”requires numeric/complex matrix/vector arguments”需要数值/复数矩阵/矢量参数错误。此时需要手工将factor变量转化为虚拟变量(哑元变量)。下面我们使用caret包中的dummyVars来进行虚拟变量转换。使用dummyVars时,有两个步骤,第一步利用dummyVars函数对数据进行哑变量处理;第二步对自身变量进行预测,并转换成data.frame格式。

#数据预处理
eneff <- read.xlsx("ENB2012_data.xlsx",sheet = 1)
names(eneff) <- c("relCompactness", "surfArea", "wallArea", 
                  "roofArea", "height", "orientation", 
                  "glazArea", "glazAreaDist", "heatLoad", "coolLoad")
#过滤所有列中有NA的行(complete.cases() 可以去除data frame中的NA行,方便后续对文本的分析和处理)
eneff <- eneff[complete.cases(eneff),]

#将朝向和玻璃面积分布转换为因子类型
eneff$orientation <- factor(eneff$orientation)
eneff$glazAreaDist <- factor(eneff$glazAreaDist)
#将所有因子类型转换为哑元变量
dummies <- dummyVars(heatLoad + coolLoad ~ ., data = eneff)
#转换完后,将两列输出结果进行合并,形成一个新的列表
eneff_data <- cbind(as.data.frame(predict(dummies, newdata = eneff)),eneff[,9:10])

#划分测试集和训练集,1到16列为特征,17,18列为输出
set.seed(474576)
eneff_sampling_vector <- createDataPartition(eneff_data$heatLoad, p = 0.80, list = FALSE)
eneff_train <- eneff_data[eneff_sampling_vector,1:16]
eneff_train_outputs <- eneff_data[eneff_sampling_vector,17:18]
eneff_test <- eneff_data[-eneff_sampling_vector,1:16]
eneff_test_outputs <- eneff_data[-eneff_sampling_vector,17:18]

#将数据进行归一化处理,压缩到0,1之间
eneff_pp <- preProcess(eneff_train, method = c("range"))
eneff_train_pp <- predict(eneff_pp, eneff_train)
eneff_test_pp <- predict(eneff_pp, eneff_test)

#同上
eneff_train_out_pp <- preProcess(eneff_train_outputs, method = c("range"))
eneff_train_outputs_pp <- predict(eneff_train_out_pp,eneff_train_outputs)
eneff_test_outputs_pp <- predict(eneff_train_out_pp,eneff_test_outputs)

library("neuralnet")
#由于neuralnet不支持使用~.的方式来自动补全变量,因此我们用下面的方法先生成公式
n <- names(eneff_data) #将列名赋给变量n
f <- as.formula(paste("heatLoad + coolLoad ~", 
                      paste(n[!n %in% c("heatLoad","coolLoad")], collapse = " + ")))

#构造了一个有一个隐藏层的神经网络。该隐藏层有10个神经元
#如果要有多个隐藏层,可以写成hidden=c(10,9),代表有2个隐藏层,第一层有10个神经元,第二层有9个神经元
#act.fct代表激活函数; linear.output 代表输出层是否是线形的
eneff_model <- neuralnet(f,data=cbind(eneff_train_pp,eneff_train_outputs_pp),hidden=10,
                         act.fct="logistic",linear.output=TRUE, err.fct="sse", rep=1)

#模型评估
#根据模型预测测试集的结果
test_predictions <- compute(eneff_model,eneff_test_pp)
#查看测试集的前6条预测结果
head(test_predictions$net.result)

#为了评估模型的好坏,我们要来计算预测值与真实值之间的差异。
#但是我们之前对结果进行了归一化,所以不能直接进行比较,需要进行数据还原。
#先查看热负荷和冷负荷在未做归一化之前的取值范围,可以看到热负荷在6.01~43.10之间
#冷负荷在10.90~47.59之间。归一化就是将6.01看作0,43.10看作1进行压缩。冷负荷也一样
eneff_train_out_pp$ranges

#还原压缩前的数据
reverse_range_scale <- function(v, ranges) {
  return( (ranges[2] - ranges[1])*v + ranges[1] )
}

test_predictions <- as.data.frame(test_predictions$net.result)
output_ranges <- eneff_train_out_pp$ranges

#循环2次(有2列),每次将该列反归一化。
test_predictions_unscaled <- 
  sapply(1:2,function(x) reverse_range_scale(test_predictions[,x],output_ranges[,x]))

mse <- function(y_p, y) {
  return(mean((y-y_p)^2))
}

mse(test_predictions_unscaled[,1],eneff_test_outputs[,1])
mse(test_predictions_unscaled[,2],eneff_test_outputs[,2])
#查看相关系数,通过相关系数也可以看出好坏
#可以看到,相关系数为0.99,接近1,代表二者相关性非常强
cor(test_predictions_unscaled[,1],eneff_test_outputs[,1])
cor(test_predictions_unscaled[,2],eneff_test_outputs[,2])

3.3 使用nnet 构建神经网络

nnet缺省的神经网络是用于分类。nnet提供了最常见的前馈反向传播神经网络算法。
我们还是使用前面逻辑回归中的玻璃数据,使用神经网络做一次预测分类:

glass <- read.csv("glass.data", header=FALSE) #读入数据
names(glass) <- c("id","RI","Na", "Mg", "Al", "Si", "K", "Ca", "Ba", "Fe", "Type") #重命名
glass$id <- NULL #去除id列。之前用的是glass <- glass[,-1],都一样。

# 划分训练集和测试集
glass$Type<- factor(glass$Type)
set.seed(4365677)
glass_sampling_vector <- createDataPartition(glass$Type, p = 0.80, list = FALSE)
glass_train <- glass[glass_sampling_vector,]
glass_test <- glass[-glass_sampling_vector,]

#归一化处理
glass_pp <- preProcess(glass_train[1:9], method = c("range"))
glass_train <- cbind(predict(glass_pp, glass_train[1:9]),Type = glass_train$Type)
glass_test  <- cbind(predict(glass_pp, glass_test[1:9]), Type = glass_test$Type)

library("nnet")
#之前这里用的是multinom,现在改用nnet
#nnet可以用.来代表所有特征,不需要像neuralne一样要写出所有特征
#size代表隐藏层的神经元个数。maxit代表最大迭代次数。nnet不能指定神经网络的层数
#decay表示衰减程度,用于控制过拟合的正则化参数。
glass_model <- nnet(Type ~ ., data = glass_train, size = 10, maxit = 1000)

#模型评估
#计算训练集的正确率
train_predictions <- predict(glass_model, glass_train[,1:9], type = "class")
mean(train_predictions == glass_train$Type)
#计算测试集的正确率
test_predictions <- predict(glass_model, glass_test[,1:9], type = "class")
mean(test_predictions == glass_test$Type)

#调参
library(caret)
nnet_grid <- expand.grid(.decay = c(0.1, 0.01, 0.001, 0.0001),
                         .size = c(50, 100, 150, 200, 250))
nnetfit <- train(Type ~ ., data = glass_train, method = "nnet", 
                 maxit = 10000, tuneGrid = nnet_grid, trace = F, MaxNWts = 10000)

3.4 使用RSNNS 构建神经网络(识别手写数字)

#用一个函数来读取图像数据,返回一个矩阵
read_idx_image_data <- function(image_file_path) {
  con <- file(image_file_path, "rb")
  magic_number <- readBin(con, what = "integer", n=1, size=4, endian="big")
  n_images <- readBin(con, what = "integer", n=1, size=4, endian="big")
  n_rows <- readBin(con, what = "integer", n=1, size=4, endian="big")
  n_cols <- readBin(con, what = "integer", n=1, size=4, endian="big")
  n_pixels <- n_images * n_rows * n_cols
  pixels <- readBin(con, what = "integer", n=n_pixels, size=1, signed = F)
  image_data <- matrix(pixels, nrow = n_images, ncol= n_rows * n_cols, byrow=T)
  close(con)
  return(image_data)
}

#读取lable
read_idx_label_data <- function(label_file_path) {
  con <- file(label_file_path, "rb")
  magic_number <- readBin(con, what = "integer", n=1, size=4, endian="big")
  n_labels <- readBin(con, what = "integer", n=1, size=4, endian="big")
  label_data <- readBin(con, what = "integer", n=n_labels, size=1, signed = F)
  close(con)
  return(label_data)
}

#读取数据
mnist_train <- read_idx_image_data("train-images.idx3-ubyte")
mnist_train_labels <- read_idx_label_data("train-labels.idx1-ubyte")

#为了快速测试,只取了前面的500行
mnist_train <- mnist_train[1:500,];
mnist_train_labels <- mnist_train_labels[1:500];

#显示图像  
display_digit <- function(image_vector, title = "") {
  reflected_image_matrix <- matrix(image_vector, nrow = 28, ncol = 28)
  image_matrix <- reflected_image_matrix[,28:1]
  gray_colors <- seq(from = 1, to = 0, by = -1/255)
  image(image_matrix, col = gray(gray_colors), xaxt='n', yaxt='n', main = title, bty="n")
}

num_images <- 10
par(mar=c(0,0,0,0)) 
layout(matrix(1:num_images, 1, num_images, byrow = TRUE))
sapply(1:num_images,function(x) display_digit(mnist_train[x,],mnist_train_labels[x]))

####################################################
# Process MNIST
####################################################

#归一化处理
mnist_input <- mnist_train / 255
#因子化lable
mnist_output <- as.factor(mnist_train_labels)

set.seed(252)
#随机抽取行数。(1到500,随机取),将训练数据的图片顺序随机化
mnist_index <- sample(1:nrow(mnist_input),nrow(mnist_input))
mnist_data <- mnist_input[mnist_index,1:ncol(mnist_input)]
mnist_out_shuffled <- mnist_output[mnist_index] # Sort the output as well

library("RSNNS")
library(Rcpp)
#decodeClassLabels这个函数会将结果集变成一个二元矩阵
#就是 c(a, b, c, b) 这样一个输入,会变成这样一个矩阵:
# rownum  a  b  c
#   1     1  0  0
#   2     0  1  0
#   3     0  0  1
#   4     0  1  0
mnist_out <- decodeClassLabels(mnist_out_shuffled)

#splitForTrainingAndTest将按比例拆成训练集和测试集,返回list,含四个元素分别就是
#训练的评价集(inputsTrain)、训练的结果集(targetTrain)、测试的评价集(inputsTest)和测试的结果集(targetTest)
mnist_split <- splitForTrainingAndTest(mnist_data, mnist_out, ratio = 0.2)
#归一化
mnist_norm <- normTrainingAndTestSet(mnist_split, type = "0_1")

#用mlp来训练神经网络。size代表神经元个数
start_time <- proc.time()
mnist_mlp <- mlp(mnist_norm$inputsTrain, mnist_norm$targetsTrain, 
                 size=100, inputsTest=mnist_norm$inputsTest, 
                 targetsTest=mnist_norm$targetsTest)
proc.time() - start_time

#模型评价
#从测试集中找到所有目标数,然后从预测后的数据中找到所有目标数,比较二者是否相等,统计相等数量
#apply(X, MARGIN, FUN, ...),margin=1 代表行操作;margin=2代表列操作,margin=c(1,2)代表行、列同时操作
mnist_class_test <- (0:9)[apply(mnist_norm$targetsTest,1,which.max)]
mlp_class_test <- (0:9)[apply(mnist_mlp$fittedTestValues,1,which.max)]
mean(mnist_class_test == mlp_class_test)
#从混淆矩阵来看,到底哪些数字没有被正确识别
confusionMatrix(mnist_class_test, mlp_class_test)

#设置图片边界
par(mar=c(2,4,2,2)) 
#将布局分为两行一列,(图片上下排列) 
#因为我们没有搞两个模型,这里可以注释掉
#layout(matrix(1:2, 2, 1, byrow = TRUE))
#绘出随迭代次数增加,误差平方和(SSE)的变化情况。
plotIterativeError(mnist_mlp, main = "Iterative Error for 100 Neuron MLP Model")
#加上图例
legend("topright", c("Training Data", "Test Data"), col=c("black", "red"), lwd=c(1,1))

#画ROC图(之前在逻辑回归中介绍过)
?plotROC
plotROC(mnist_mlp$fittedTestValues[,2],mnist_norm$targetsTest[,2], 
        main="ROC Curve for 300 Neuron MLP Model (Digit 1)", 
        xlab="1 - Specificity", ylab = "Sensitivity")
abline(a = 0, b = 1, lty=2)
legend("bottomright", c("Model", "Random Classifier"), lty = c(1,2), lwd=c(1,1))

【参考文献】
神经网络——最易懂最清晰的一篇文章
“反向传播算法”过程及公式推导
一文搞懂反向传播算法
机器学习入门笔记:PLA算法
ROC的计算与绘制

你可能感兴趣的:(机器学习(五)——神经网络)