来写个逻辑回归吧(一)

前言

说来惭愧, 博客已经很久没更新了, 动过好几次笔, 写出来的东西自己都不忍心再看第二遍, 不想发上去浪费看客的时间

自己给自己的理由是这段时间的积累太少, 没法形成有效的总结

所以打算先静下心来做点没有过错的事情

技术总是没有过错的, 写点最近学习的一些事情吧

最近在看吴恩达的机器学习的课程(链接在这里https://mooc.study.163.com/smartSpec/detail/1001319001.htm, 网易的课程), 按照第一部分的讲解, 试图写一个逻辑回归, 完成一个经典的猫狗识别的场景应用

准备工作

  1. 下载素材, 地址: https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data, 训练集有25000张图片, 猫狗各占一半, 测试集有12500, 没有标明是猫还是狗
  2. 安装python环境, 执行以下命令安装所需环境:
yum install python, pip
pip install numpy
yum install opencv, opencv-python

目录

数据准备好之后, 我们创建的目录结构如下

xbsura@ali> ls
code  data  lr.ipynb
xbsura@ali> ls data/
test  train

code下为python代码, data下为所有图片, 分别放置在train和test下

数据处理

下载下来的数据为图片二进制, 首先需要将其格式化为矩阵, 这些图片大小不一, 为了方便计算, 先将其变换为一致的像素, 并按照转化后三个色彩值得先后顺序变换为列数为1, 行数为$$widthheight3$$的矩阵, 在numpy中是ndarray(($$widthheight3,1$$))

在标记位上, 认为是狗为1, 不是狗为0

我们使用opencv处理

import cv2
import numpy as np

m = 50 # 学习的图片张数目, 其中一半是狗, 一半是猫
height = 50 # 图片格式化的高度
width = 50 # 图片格式化的宽度
n = width*height*3 # 图片维度

trains = np.ndarray((m, n, 1))
labels = np.ndarray((m, 1))

for i in range(m/2):
    dog = cv2.imread("data/train/dog.%d.jpg" % i)
    cat = cv2.imread("data/train/cat.%d.jpg" % i)
    trains[2*i] = cv2.resize(dog, (width, height)).reshape((n, 1)) / (255.0 * n) # 归一化一下维度值
    labels[2*i][0] = 1
    trains[2*i+1] = cv2.resize(cat, (width, height)).reshape((n, 1)) / (255.0 * n)
    labels[2*i+1][0] = 0

逻辑回归

使用逻辑回归, 我们假设存在一个$$w$$, 他和结果值$$y$$之间存在$$y = wx + b$$的关系, 同时, 为了将$$y$$值限定在一定的范围内, 我们对线性变换的结果进行处理, 通常使用一个名为sigmoid的方法将$$y$$值限定定在(0,1)之间, sigmoid定义为 $$sigmoid(x)=\frac{1}{1 + e^{-x}}$$

所以我们最终的假设是: $$\hat{y} = sigmoid(wx + b)$$

其中, $$w$$为行数为1的矩阵, $$x$$为我们的训练集合, $$b$$为常数, 我们可以假设初始值都为0, 之后使用梯度下降法对各个元素进行逼近求解

不同的$$w$$和$$b$$预测结果是不同的, 把总体误差的最小化作为我们训练模型的目标, 需要精确化定义误差, 我们把总体误差称为代价函数, 用$$J$$表示, 总体的$$J$$定义为 损失函数 的平均值, 损失函数 是指在每个样本上的误差值, 用$$L$$表示, $$L$$的选取是个数学问题, 为了使得梯度下降能够使得代价函数的值全局收敛, 我们使用的损失函数为: $$L = -({y}log{\hat{y}}+(1-y)log(1-\hat{y}))$$, 所以代价函数为: $$J=\frac{1}{n}\sum_{i=1}^{n}L$$

梯度下降是一个过程, 通过每次向最小化$$J$$函数的方向步进, 来提高模型的质量

首先需要求$$\frac{dJ}{dw}$$ 和 $$\frac{dJ}{db}$$, (s在此处表示线性函数$$wx+b$$)

$$\frac{dJ}{dw} = \frac{dJ}{dL} * \frac{dL}{d\hat{y}} * \frac{d\hat{y}}{ds} * \frac{ds}{dw} = \frac{1}{n}\sum_{i=1}^{n} \frac{\hat{y} – y}{\hat{y}(1-\hat{y})} * \hat{y}(1-\hat{y}) * x = \frac{1}{n}\sum_{i=1}^{n} x(\hat{y} – y)$$

$$\frac{dJ}{db} = \frac{dJ}{dL} * \frac{dL}{d\hat{y}} * \frac{d\hat{y}}{ds} * \frac{ds}{db} = \frac{1}{n}\sum_{i=1}^{n} \frac{\hat{y} – y}{\hat{y}(1-\hat{y})} * \hat{y}(1-\hat{y}) = \frac{1}{n}\sum_{i=1}^{n} (\hat{y} – y)$$

之后, 使用$$w = w – a\frac{dJ}{dw}, b = b – a\frac{dJ}{db}$$对$$w$$和$$b$$进行求值, 在运算时, 把$$w$$和$$b$$都初始化为0, 步进值设置成1, 轮数设置成50次, 看一下效果

import time

w = np.zeros((1, n))
b = 0

loop = 50
a = 1

jFunctions = []
correctRate = []

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

for l in range(loop):
    startTime = time.time()
    jFunction = np.float(0)
    for i in range(m):
        try:
            y = labels[i][0]
            yhat = sigmoid(np.dot(w, trains[i])[0][0] + b)
            if yhat == 1.0:
                print np.dot(w, trains[i])[0][0], b
            jFunction = float(jFunction + -(y*np.log(yhat) + (1-y) * np.log(1-yhat)))
        except Exception as e:
            jFunction = jFunction + 0
    jFunction = jFunction / m

    dw = np.zeros((n, 1))
    db = 0
    for j in range(n):
        for k in range(m):
            dw[j][0] = dw[j][0] + trains[k][j][0] * (sigmoid(np.dot(w, trains[k])[0][0] + b) - labels[k][0])
        dw[j][0] = dw[j][0] / m
    for k in range(m):
        db = db + (sigmoid(np.dot(w, trains[k])[0][0] + b) - labels[k][0])
    db = db / m

    for i in range(n):
        w[0][i] = w[0][i] - a * dw[i][0]
    b = b - a * db

    s = 0
    for i in range(m):
        r = sigmoid(np.dot(w, trains[i])[0][0] + b)
        if r <= 0.5 and labels[i][0] == 0:
            s = s + 1
        if r > 0.5 and labels[i][0] == 1:
            s = s + 1
    jFunctions.append(jFunction)
    correctRate.append(s*100.0/m)

在进行训练后, 我们得到了一个模型, 看一下损失函数的变化情况和准确率的变化情况

import pylab as pl
pl.grid()
x = range(loop)
y = correctRate
pl.xlabel("loop(times)")
pl.ylabel("correctRate(%)")
pl.plot(x, y)
pl.show()

pl.grid()
y = jFunctions
pl.xlabel("loop(times)")
pl.ylabel("jFunctions(%)")
pl.plot(x, y)
pl.show()


小节

到此为止, 一个可运行的逻辑回归完成了, 回看整个过程, 遇到的问题有:
1. 步进参数的调整问题, 一开始没有对像素值做变换, 每个值都在0-255之间, 步进值很难确定应该从多少开始调整, 后来将像素值根据采样大小做了变换, 使所有的值和小于1之后, 步进值可以选取为1
2. 运行速度的问题, 在阿里云低配机器上, 这样的训练过程, 训练一个50样本7500维度50轮的模型花了5分钟时间, 真正跑完全部数据会花费好几天时间

下一篇文章, 会有以下内容:
1. 利用操作的矩阵化来加快速度
2. 介绍模型效果的一些评判指标
3. 调参以优化模型效果
4. 完成一个模型的线上应用

打赏

2 thoughts to “来写个逻辑回归吧(一)”

评论已关闭