笨方法学机器学习(三):卷积神经网络

网络结构的解析

  • 拥有不同的层,不同的网络层有不同的功能
卷积神经网络示意图
  • 输入层:数据的预处理
  • conv卷积层:卷积神经网路的核心网络层,用一组权重通过窗口滑动同时计算输入层
  • relu 激活函数层,把卷积层的结果做非线性映射
  • pool 池化层,压缩数据和参数的量,减少过拟合
  • fc 全连接层。神经网络中的全连接层,通常在卷积神经网络的尾部,用来更好的拟合数据

一般的的卷积神经网络结构为:
$$input->[[conv->relu]*N]->poll] *M->[fc -> relus] *K$$
其中N,M,k代表了层结构的数量

上图中网络结构我们可以表示为:
$$ INPUT -> [[CONV-relu]1 -> POOL]2 -> [FC]*2$$

即是:$$N=1,M=2,K=2$$

我们看到卷积神经网络跟全连接神经网络不同之处在于:

  • 卷积神经网络的神经元是按三维排列的,有用其深度宽度高度

图说明:

从上面的图表示的神经网络中,

  1. 输入层的宽度跟高度跟图像的宽度跟高度对应,其深度为1
  2. 第一个卷积层对输入层的图像进行了卷积处理,得到3个feature map,'3个'因为这个卷积层包含了3Filter(三个不同的权值矩阵),每个Filter都可以把输入层的数据卷积到一个Feature map. 通常Filter 也被称作通道(channel)
  3. 在第一个卷积层后,Pooling层对三个feature map做了一个下采样处理,得到了三个更小的feature map
  4. 然后到了第二层卷积层,5个filter 对上一层的三个Feature map处理得到新的5个Feature Map,接着继续进行Pooling层,得到5个更小的Feature Map,然后就是第二个Pooling层,进行下采样,,这样得到5个更小的Feature Map
  5. 最后两层是全连接层,第一个全连接层的每个神经元,和上一层的5个Feature Map中的每个神经元连接,第二个全连接层(输出层),则与第一个全连接层的神经元相连,这样得到整个网络的输出

一、输入层:

  • 输入层一般进行数据的处理

  • 去均值:
    如图:



    左边是原始数据,右边是去均值后的数据
    它将数据中心整体移动到0点,方便做计算(做求导计算的时候减少计算量)

  • 归一化:
    归一化是为了减少计算范围,避免某一个特征维在计算的时候比重过大(在决策树中使用过)
    归一化公式:$$a=a/(max-min)$$
    它将所有维的特征值的变化都缩小到0-1的范围内

二、Relu层(激活层)

之所以叫relu层是因为这个层是采用Relu函数作为激活函数,对卷积层的输出进行激活处理,一般跟Relu层跟卷积层合在一层叫做卷积层.

Relu函数的定义是:
$$F(x)=max(0,x)$$

它的导数是分段的:
$f'(x)=0 ,x<0$
$f'(x)=1 ,x>0$

当然,你也可以选择其他函数做为卷积层的激励函数:
比如:

  • Sigmid
  • Tanh
  • Leaky Relu

三、卷积层的解析

(1)卷积的计算

假设有一个5*5的图像,使用一个3*3的filter进行卷积,想得到一个3*3的Feature Map,如下所示:

我们对矩阵的元素进行标记:

表示图像矩阵的第i行第j列元素,用

代表Filter矩阵的第m行,第n列权重,用
表示Filter的偏置项,用
代表了feature map的第i行第j列元素,用f 表示激活函数,这里的激活函数我们选取Relu激活函数,然后使用一下公式计算卷积:

(即在第0,0格中,3*3的窗口固定在image矩阵的左上角位置,3*3的矩阵与3*3的Filter矩阵对应位置相乘然后取和,然后其激活函数的输出作为输出矩阵的第0,0格的值)
如:


第二个位置时候(A0,1):
3*3的窗口往右移动一个步长(strike,这个例子步长为1):然后依次对应位置相乘取和,再通过激活函数,得到A0,1的值:


下面的动图展示了Feature Map的每个位置的计算:


上面的情况是步长为1的计算过程,事实上,步长就是窗口(与Filter同样大小的)每次移动的距离,当步长为2的时候:



我们看到Feature Map的大小跟步长的设定有关系:

  • W2是卷积后Feature Map的宽度,W1是卷积前的图像宽度,F是Filter的宽度,P是Zero Pading的数量(指在原始图像周围补几圈0),S是步长,
  • H同理是其高度表示

当卷积层的深度大于1,即多个Filter叠在一起同时计算时,只要把深度那一维也同时进行对应位置相乘然后加到之前的和里面经过激励函数输出即可,计算公式为:



下面的动图介绍了RGB三维图像经过2个3*3*3的Filter后计算过程:

这里体现了一些卷积神经网络的特征:

  • 权重共享 Filter的权值对于上一层的所有神经元都是一样的
  • 局部相连:每一层的神经元只与上一层的部分神经元相连

对于两个3*3*3的卷积层来说,其参数数量仅有(3*3*3+1)*2=56个,且参数数量与上一层神经元个数无关.

卷积公式表示卷积层

数学中的二维卷积公式为:


我们可以将其表示为:
$$C=A*B$$

我们如果按上面公式计算,其实会发现A其实是Filter,而矩阵B是待输入的矩阵,而位置关系也不同:


卷积层中的"卷积"看上去是将input矩阵旋转了180°进行计算然后把AB位置调换进行的卷积计算,这种计算其实叫做'互相关'

如果我们不考虑这些小的差别的话.步长为1的卷积层计算我们可以简化公式为:


四、Pooling层

Pooling层也叫池化层或者下采样层,它将卷积层得到的输出进行采集"有用"数据并抽象化,从而减少参数减少计算量.
池化方法常用的有:Max Pooling 跟Mean Pooling两种,
前者求窗口最大值作为池化后的该格子的输出值,后者取平均值作为格子输出值


Pooling 层的深度跟卷积层数深度一样,所以是卷积的各层Feature Map分别做Pooling处理

全连接层

全连接层的输出值计算与训练与上篇一样

训练与数学推导(数学公式重灾区)

整个卷积神经网络的训练我们都采用反向传播(Back Propagatio)算法进行训练,算法 的介绍为:


其中损失函数$Ed$的定义为:
取网络所有输出层节点的误差平方作为损失函数:


卷积误差项的传递

我们先来考虑步长为1,输入深度为1,Filter个数为1的最简单情况:
假设输入的大小为3*3,filter大小为2*2,按步长为1卷积,我们将得到2*2的feature map。如下图所示:


在这里,我们假设第l层的每个误差项

都是已知的,我们需要求上一层的误差项

根据链式法则:

我们先来求第一项:



我们发现,计算

,相当于把第l层的sensitive map 周围补一圈0,然后再跟旋转180°的Filter进行互相关cross-correlation(卷积层的对应位置相乘然后相加操作)
如图:

因为数学意义的[卷积]和互相关操作是可以转化的。首先,我们把矩阵A翻转180度,然后再交换A和B的位置(即把B放在左边而把A放在右边。卷积满足交换率,这个操作不会导致结果变化),那么卷积就变成了互相关。
那么,上面的计算可以用卷积公式表示:



Wl代表了第l层的Filter的权重矩阵,
和的形式:


现在我们来看第二项

:



所以只要对激活函数Relu求导即可:

所以两项相乘:



改写成卷积形式:

代表了互相关操作.
这样步长为1、输入的深度为1、filter个数为1的最简单的情况,卷积层误差项传递的算法。

步长为S的时候:

我们可以看出,因为步长为2,得到的feature map跳过了步长为1时相应的部分。

因此,当我们反向计算误差项的时候,我们可以对步长为S的Feature Map的响应位置进行补0,将其[还原]成步长为1时的sensitivity map,再用以下式子进行计算

Filter数量为N时候的误差传递:



D为深度

卷积Filter权重梯度的计算:


计算

:

由于是权值共享,权值
的计算对所有的
都有影响,根据全导数公式:

计算:

所以其公式是:


偏置项的梯度:



所有sensivity map所有的误差项之和.

对于步长为S的卷积层,处理方法跟传递误差项是一样的:

  • 将sensitivity map还原成步长为1的sensitivity map,
  • 再用上面的方法计算

获得了所有的梯度之后,根据梯度下降算法更新每个权重.

梯度下降,简单来说就是:
$$w=w-f'(w)$$

这样,我们就解决了卷积层的训练

Pooling层的训练:

max pooling 跟Mean pooling都没有参数,所以需要将误差项传递到上一层

max Pooling层

不妨设最大值为

所以得:



所以,对于max pooling,下一层的误差项的值会原封不动的传递到上一层区块的最大值对应的神经元,而其他神经元的误差项的值都是0

mean Pooling层


所以对于mean Pooling层,下一层的误差项会平均分配到上一层所对应 的区块中.


全连接层

参看 神经网络这篇博客的介绍:

代码实现:

导入工具包:

import numpy as np

卷积层的初始化:

class ConvLayer(object):
    def __init__(self,input_width,input_height
                 ,channel_number,filter_width
                 ,filter_height,filter_number
                 ,zero_padding,stride,activator,
                 learning_rate):
        '''
        :param input_width: 输入矩阵的宽
        :param input_height:输入矩阵的高
        :param channel_number:
        :param filter_width:共享权重的filter矩阵宽
        :param zero_padding:补几圈0
        :param stride:窗口每次移动的步长
        :param activator:激励函数
        :param learning_rate:学习率
        :param filter_height共享权重的filter矩阵宽
        :param filter_number filter的深度
        '''
        self.input_width = input_height
        self.input_height = input_height
        self.channel_number = channel_number
        self.filter_width = filter_width
        self.filter_height = filter_height
        self.filter_number = filter_number
        self.zero_padding = zero_padding
        self.stride = stride
        self.activator = activator
        self.learning_rate = learning_rate
        self.output_height = ConvLayer.calculate_output_size(
            self.input_height,filter_height,zero_padding,stride
        )
        self.output_width = ConvLayer.calculate_output_size(
            self.input_width,self.filter_width,zero_padding,stride
        )
        self.output_array = np.zeros((self.filter_number,
                                      self.output_height,
                                      self.output_width))
        self.filters = []
        for i in range(filter_number):
            self.filters.append(Filter(filter_width,
                                       filter_height,
                                       self.channel_number))

calculate_output_size函数用来计算卷积层输出:

@staticmethod
    def calculate_output_size(input_size,filter_size
                              ,zero_padding,stride):
        return int( (input_size - filter_size + 2 * zero_padding) / stride + 1)

定义Filter类与Relu激活函数:

class Filter(object):
    #Filter 类 保存了卷积层的参数以及梯度,并用梯度下降的办法更新参数
    #权重随机初始化为一个很小的值,而偏置项初始化为0。
    def __init__(self,width,height,depth):
        self.weights = np.random.uniform(-1e-4,1e-4,(depth,height,width))
        self.bias =0
        self.weights_grad = np.zeros(self.weights.shape)
        self.bias_grad = 0
    def __repr__(self):
        return 'filter weights:\n%s\nbias:\n%s' % (
            repr(self.weights), repr(self.bias))
    def get_weights(self):
        return self.weights

    def get_bias(self):
        return self.bias

    def update(self,learning_rate):
        self.weights -= learning_rate * self.weights_grad
        self.bias -= learning_rate * self.bias_grad
class ReluActivator(object):
    def forward(self,weighted_input):
        return max(0,weighted_input)
    def backward(self,output):
        return 1 if output > 0 else 0

卷积层前计算:

    def forward(self,input_array):
        '''
        计算卷积层的输出
        :param input_array: 前一层的输出
        :return: 没有返回,输出结果保存到self.output_array
        '''
        self.input_array = input_array
        self.padded_input_array = padding(input_array,self.zero_padding)
        for f in range(self.filter_number):
            filter = self.filters[f]
            conv(self.padded_input_array,filter.get_weights(),
                 self.output_array[f],self.stride,filter.get_bias())
        element_wise_op(self.output_array,self.activator.forward)

前计算用到的 工具函数:

def padding(input_array, zp):
    '''
    将输入矩阵补0
    :param input_array:
    :param zp: 补0的圈数
    :return:
    python3 玄学除法,int 变float
    '''
    zp = int(zp)
    if zp ==0:
        return input_array
    else:
        if input_array.ndim==3:
            input_width = input_array.shape[2]
            input_height = input_array.shape[1]
            input_depth = input_array.shape[0]
            padder_array = np.zeros((input_depth,input_height+2*zp,input_width+2*zp))
            padder_array[:,zp:zp+input_height,zp:zp+input_width]=input_array
            return padder_array
        elif input_array.ndim==2:
            input_height = input_array.shape[0]
            input_width = input_array.shape[1]
            padder_array = np.zeros((input_height+2*zp,input_width+2*zp))
            padder_array[zp:zp+input_height,zp:zp+input_width]=input_array
            return padder_array
def element_wise_op(array, op):
    '''
    对numpy数组元素依次进行op操作(这里是函数)
    :param array:
    :param op:
    :return:
    '''
    for i in np.nditer(array,
                       op_flags=['readwrite']):
        i[...] = op(i)
def conv(input_array,kernel_array,output_array,stride,bias):
    '''
    计算卷积
    :param input_array:
    :param kernel_array:
    :param output_array:
    :param stride:
    :param bias:
    :return:
    '''
    channel_number = input_array.ndim
    output_width = output_array.shape[1]
    output_height = output_array.shape[0]
    kernel_width = kernel_array.shape[-1]
    kernel_height = kernel_array.shape[-2]
    for i in range(output_height):
        for j in range(output_width):
            #依次计算每一格的卷积
            output_array[i][j] = (get_patch(input_array,i,j,
                                            kernel_width,kernel_height,stride) * kernel_array ).sum()+bias
def get_patch(input_array, i, j, kernel_width,
                    kernel_height, stride):
    '''
    获得窗口移动后input的array
    '''
    i*=stride
    j*=stride
    max_height = i + kernel_height
    max_width = j + kernel_width
    if input_array.ndim == 3:
        max_z = input_array.shape[0] + 1
        return input_array[0:max_z, i:max_height, j:max_width]
    else:
        return input_array[i:max_height, j:max_width]

卷积层的反向传播代码,这些方法都在ConvLayer类中:

    def bp_sensitivity_map(self,sensitivity_array,activator):
        '''
        卷积层反向传播算法的实现
        1,将误差项传递到上一层
        2:计算每个参数的梯度
        3:更新参数
        :param sensitivity_array: 本层的sensitivity map
        :param activator: 上一层的激活函数
        :return:
        '''
        expanded_array = self.expand_sensitivity_map(sensitivity_array)
        #full 卷积
        expanded_width = expanded_array.shape[2]
        #获得补0 数
        zp = (self.input_width+self.filter_width-1-expanded_width)/2
        padded_array = padding(expanded_array,zp)
        #创建初始误差矩阵
        self.delta_array = self.create_delta_array()

        #对于具有多个filter的卷积层来说,最终传递到上一层的sensitivity map
        #相当于把所有的filter的sensitivity map之和
        for f in range(self.filter_number):
            filter = self.filters[f]
            # 将filter权重翻转180度

            flipped_weights = np.array(map(
                lambda i: np.rot90(i, 2),
                filter.get_weights()))
            #python3运行的时候这个map有问题
            #计算每一个filter的delta_array
            delta_array = self.create_delta_array()
            for d in range(delta_array.shape[0]):
                conv(padded_array[f],flipped_weights[d],delta_array[d],1,0)
            self.delta_array+=delta_array
        #创建激活函数矩阵(卷积反向传播误差项的第二项)
        derivative_array = np.array(self.input_array)
        element_wise_op(derivative_array,self.activator.backward)
        self.delta_array *= derivative_array

    def bp_gradient(self,sensitivity_array):
        '''
        计算梯度,包括权重跟偏置项
        :param sensitivity_array:
        :return:
        '''
        expanded_array = self.expand_sensitivity_map(sensitivity_array)
        for f in range(self.filter_number):
            filter = self.filters[f]
            for d in range(filter.get_weights().shape[0]):
                conv(self.padded_input_array[d],expanded_array[f],
                    filter.weights_grad[d],1,0)
            filter.bias_grad = expanded_array[f].sum()
    def expand_sensitivity_map(self,sensitivity_array):
        '''
        将步长为S的map 还原成步长1的map
        :param sensitivity_array:
        :return:
        '''
        expanded_depth = sensitivity_array.shape[0]
        expanded_height = (self.input_height-self.filter_height+2*self.zero_padding+1)
        expanded_width = (self.input_width-self.filter_width+2*self.zero_padding+1)
        expanded_array = np.zeros((expanded_depth,expanded_height,expanded_width))
        for i in range(self.output_height):
            for j in range(self.output_width):
                i_pos = i * self.stride
                j_pos = j * self.stride
                expanded_array[:,i_pos,j_pos]=sensitivity_array[:,i,j]
        return expanded_array
    def create_delta_array(self):
        return np.zeros((self.channel_number,self.input_height,self.input_width))
    def update(self):
        '''
        更新这一层的权重跟偏置项,很简单依次更新每一个filter就行了
        :return:
        '''
        for filter in self.filters:
            filter.update(self.learning_rate)
    def backward(self, sensitivity_array, activator=None):
        if not activator:
            activator = self.activator
        self.bp_sensitivity_map(sensitivity_array, activator)
        self.bp_gradient(sensitivity_array)

为了验证我们的卷积层是否写得正确,验证代码:

from conv import *
#conv是我卷积层类所在的python文件,此处导入模块
import numpy as np
class IdentityActivator(object):
    def forward(self, weighted_input):
        #return weighted_input
        return weighted_input
    def backward(self, output):
        return 1
def init_test():
    a = np.array(
        [[[0,1,1,0,2],
          [2,2,2,2,1],
          [1,0,0,2,0],
          [0,1,1,0,0],
          [1,2,0,0,2]],
         [[1,0,2,2,0],
          [0,0,0,2,0],
          [1,2,1,2,1],
          [1,0,0,0,0],
          [1,2,1,1,1]],
         [[2,1,2,0,0],
          [1,0,0,1,0],
          [0,2,1,0,1],
          [0,1,2,2,2],
          [2,1,0,0,1]]])
    b = np.array(
        [[[0,1,1],
          [2,2,2],
          [1,0,0]],
         [[1,0,2],
          [0,0,0],
          [1,2,1]]])
    cl = ConvLayer(5,5,3,3,3,2,1,2,IdentityActivator(),0.001)
    cl.filters[0].weights = np.array(
        [[[-1,1,0],
          [0,1,0],
          [0,1,1]],
         [[-1,-1,0],
          [0,0,0],
          [0,-1,0]],
         [[0,0,-1],
          [0,1,0],
          [1,-1,-1]]], dtype=np.float64)
    cl.filters[0].bias=1
    cl.filters[1].weights = np.array(
        [[[1,1,-1],
          [-1,-1,1],
          [0,-1,1]],
         [[0,1,0],
         [-1,0,-1],
          [-1,1,0]],
         [[-1,0,0],
          [-1,0,1],
          [-1,0,0]]], dtype=np.float64)
    return a, b, cl
def gradient_check():
    '''
    梯度检查
    '''
    # 设计一个误差函数,取所有节点输出项之和
    error_function = lambda o: o.sum()
    # 计算forward值
    a, b, cl = init_test()
    cl.forward(a)
    # 求取sensitivity map,是一个全1数组
    sensitivity_array = np.ones(cl.output_array.shape,
                                dtype=np.float64)
    # 计算梯度
    cl.backward(sensitivity_array,
                  IdentityActivator())
    cl.update()
    # 检查梯度
    epsilon = 10e-4
    for d in range(cl.filters[0].weights_grad.shape[0]):
        for i in range(cl.filters[0].weights_grad.shape[1]):
            for j in range(cl.filters[0].weights_grad.shape[2]):
                cl.filters[0].weights[d,i,j] += epsilon
                cl.forward(a)
                err1 = error_function(cl.output_array)
                cl.filters[0].weights[d,i,j] -= 2*epsilon
                cl.forward(a)
                err2 = error_function(cl.output_array)
                expect_grad = (err1 - err2) / (2 * epsilon)
                cl.filters[0].weights[d,i,j] += epsilon
                print('weights(%d,%d,%d): expected - actural %f - %f' % (
                    d, i, j, expect_grad, cl.filters[0].weights_grad[d,i,j]))
gradient_check()
image.png

Max Pooling 层的实现:

# -*- coding:utf-8 -*-
#!/usr/bin/local/bin/python
import numpy as np
from tools import *
class MaxPoolingLayer(object):
    def __init__(self,input_width,input_height,
                 channel_number,filter_width,
                 filter_height,stride):
        self.input_width = input_width
        self.input_height = input_height
        self.channel_number = channel_number
        self.filter_width = filter_width
        self.filter_height = filter_height
        self.stride = stride
        self.output_width = (input_width-filter_width)/self.stride + 1
        self.output_height = (input_height - filter_height)/self.stride +1
        self.output_array = np.zeros((self.channel_number,
                                      self.output_height,self.output_width))

    def forward(self,input_array):
        self.input_array = input_array
        for d in range(self.channel_number):
            for i in range(self.output_height):
                for j in range(self.output_width):
                    self.output_array[d,i,j] = (get_patch(
                        input_array[d],i,j,
                        self.filter_width,
                        self.filter_height,
                        self.stride
                    ).max())

    def backward(self,sensitivity_array):
        self.delta_array = np.zeros(self.input_array.shape)
        for d in range(self.channel_number):
            for i in range(self.output_height):
                for j in range(self.output_width):
                    patch_array = get_patch(
                        self.input_array[d],i,j,
                        self.filter_width,
                        self.filter_height,
                        self.stride
                    )
                    k,l =get_max_index(patch_array)
                    self.delta_array[d,
                                     i*self.stride+k,
                                    j*self.stride+l] = sensitivity_array[d,i,j]
    def update(self):
        #因为不需要进行更新权重,所以此方法pass,但是为了保证整个网络更新的时候可以用layers.update()方法统一更新权值,所以写了个空方法
        pass

tools文件定义了两个工具方法:

# -*- coding:utf-8 -*-
import numpy as np
def get_patch(input_array, i, j, kernel_width,
                    kernel_height, stride):
    '''
    获得窗口移动后input的array
    '''
    i*=stride
    j*=stride
    max_height = i + kernel_height
    max_width = j + kernel_width
    if input_array.ndim == 3:
        max_z = input_array.shape[0] + 1
        return input_array[0:max_z, i:max_height, j:max_width]
    else:
        return input_array[i:max_height, j:max_width]
def get_max_index(arr):
    '''
    获取数组中的最大值,返回坐标
    :param arr:
    :return:
    '''
    idx = np.argmax(arr)
    return (int(idx / arr.shape[1]), idx % arr.shape[1])

全连接层 fc.py:

# -*- coding:utf-8 -*-
#!/usr/bin/local/bin/python

import numpy as np

class FullConnectedLayer(object):
    def __init__(self,input_size,
                 output_size,
                 learing_rate,
                 activator):
        self.input_size = input_size
        self.output_size = output_size
        self.activator = activator
        self.learning_rate = learing_rate
        self.W = np.random.uniform(-0.1,0.1,(output_size,input_size))
        self.b = np.zeros((output_size,1))
        self.output = np.zeros((output_size,1))

    def forward(self,input_array):
        self.input = input_array
        self.output = self.activator.forward(
            np.dot(self.W,input_array)+self.b
        )

    def backward(self,delta_array):
        self.delta = self.activator.backward(self.input) * np.dot(
            self.W.T,delta_array
        )
        self.W_grad = np.dot(delta_array,self.input.T)
        self.b_grad = delta_array

    def update(self):
        self.W += self.learning_rate * self.W_grad
        self.b += self.learning_rate * self.b_grad

这里我们完成了卷积层,pooling(池化)层,全连接层的定义,我们就可以用这些简陋的轮子搭建一个简单的卷积神经网络了.
为什么写这些代码而不用框架:

  • 框架是别人写好的轮子,只会用框架不能理解其中的含义
  • 自己写的代码更容易理解其中的算法与数学含义
  • 个人建议,学习阶段写轮子,使用阶段用别人的轮子

我用上面的代码搭建了一个巨简陋的卷积神经网络去试试MNIST手写数字数据集的识别
完整代码在:

git clone  https://github.com/coldsummerday/mllearn.git
cd mllearn-master/cnn/
#建议用python2,用python3的朋友吧conv文件中的bp_sensitivity_map中有个map方法改成python3的即可
python main.py 
#good luck!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容