[python][机器学习][scipy]使用稀疏矩阵实现卷积运算

最后一次更新日期: 2019/3/21

此篇文章提供一个将scipy库的稀疏矩阵运用于卷积运算上,以期在不增加过多内存开销的情况下提高性能的入门思路。

先导入以下模块:
import numpy as np
from scipy import sparse
import time

1.基于滑窗的卷积运算

卷积核

卷积核的三个轴分别对应:高,宽,颜色通道

In [5]: kernel=np.eye(3).repeat(3).reshape((3,3,3))

In [6]: kernel[:,:,0]
Out[6]: 
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

图片数据

cifar10数据集作为示例,加载数据集的方法此处不作讲解。
数据集的四个轴分别对应:样本,高,宽,颜色通道

In [11]: cifar=CifarManager()
    ...: train_images,train_labels,test_images,test_labels=cifar.read_as_array(chinese_label=True)
    ...: The default path of image data is set to:
    ...: D:\training_data\used\cifar-10-batches-py
    ...: reading data---
    ...: completed

In [12]: test_images.shape
Out[12]: (10000, 32, 32, 3)

In [13]: Image.fromarray(test_images[0].astype('uint8')).resize((200,200))
Out[13]: 

图片数据集的数据结构如下图所示:

axis0对应样本,axis1对应图片的高,axis2对应图片的宽,axis3对应颜色通道。
从中抽取一张图片,则axis0,axis1,axis2分别对应高,宽,通道

单图卷积运算

#array: 单张图片数据
#kernel: 卷积核
#step: 步长
def conv1(array,kernel,step=1):
    #图片的高和宽
    h,w=array.shape[:2]
    #卷积核的高和宽
    kh,kw=kernel.shape[:2]
    #纵向和横向的移动步数
    hs,ws=(h-kh)//step+1,(w-kw)//step+1
    #初始化输出数组
    out=np.empty((hs,ws))
    #纵向滑动卷积核
    for i in range(hs):
        #卷积核的纵向覆盖范围
        start0,end0=i*step,i*step+kh
        #横向滑动卷积核
        for j in range(ws):
            #卷积核的横向覆盖范围
            start1,end1=j*step,j*step+kw
            #该位置的卷积运算
            out[i,j]=np.vdot(array[start0:end0,start1:end1],kernel)
    return out

In [37]: out=conv1(test_images[0],kernel)

In [38]: out.shape
Out[38]: (30, 30)

In [39]: out_=(out-out.min())/(out.max()-out.min())*255
    ...: Image.fromarray(out_.astype('uint8')).resize((200,200))
Out[39]: 

np.vdot用于将数组展开为向量后进行点积运算。

卷积运算最为简单的实现方式是滑窗,用卷积核在每个位置上覆盖图像数据,对应元素相乘再求和,存入结果数组的相对位置。

单张图片,单个卷积核,在某一个位置的卷积运算如下图所示:

批量卷积运算

#array: 图片数据集
#kernel: 卷积核
#step: 步长
def conv2(array,kernel,step=1):
    #图片的张数、高、宽
    n,h,w=array.shape[:3]
    #卷积核的高、宽、颜色通道
    kh,kw,kc=kernel.shape[:3]
    #纵向和横向的移动步数
    hs,ws=(h-kh)//step+1,(w-kw)//step+1
    #初始化输出数组
    out=np.empty((n,hs,ws))
    #纵向滑动卷积核
    for i in range(hs):
        #卷积核的纵向覆盖范围
        start0,end0=i*step,i*step+kh
        #横向滑动卷积核
        for j in range(ws):
            #卷积核的横向覆盖范围
            start1,end1=j*step,j*step+kw
            #该位置的卷积运算
            array_=array[:,start0:end0,start1:end1].reshape((-1,kh*kw*kc))
            kernel_=kernel.ravel()
            out[:,i,j]=np.dot(array_,kernel_)
    return out

In [50]: start=time.clock()
    ...: out2=conv2(test_images,kernel)
    ...: print('\ntime used: %f s'%(time.clock()-start))

time used: 2.255798 s

In [51]: out2.shape
Out[51]: (10000, 30, 30)

In [52]: (out==out2[0]).all()
Out[52]: True

np.dot可以用于完成 向量点积,矩阵与向量的乘积,矩阵乘法 等运算。

在进行某一位置的卷积运算时,先在数据集上提取卷积核覆盖范围的数据,如上方的例子,可以得到一个形状为(10000,3,3,3)的张量,第一个轴对应每张图片,后三个轴对应卷积核的三个轴。将子数据集的后三个轴展开,形状变为(10000,27),卷积核展开为向量,形状变为(27,),然后进行一次矩阵和列向量的乘积运算就得到卷积结果了。

(为方便描述,公式中皆以1为起始索引)
以位置1,1为例,n,h,w,i,j分别对应样本数,图片的高,图片的宽,纵向滑动的步数,横向滑动的步数,计算的主要过程如下:

(1).第一张图片的数据:(\left[ \begin{matrix} a_{111} & a_{121} & \cdots & a_{1w1}\\ a_{211} & a_{221} & \cdots & a_{2w1}\\ \vdots & \vdots & \ddots & \vdots\\ a_{h11} & a_{h21} & \cdots & a_{hw1}\\ \end{matrix} \right], \left[ \begin{matrix} a_{112} & a_{122} & \cdots & a_{1w2}\\ a_{212} & a_{222} & \cdots & a_{2w2}\\ \vdots & \vdots & \ddots & \vdots\\ a_{h12} & a_{h22} & \cdots & a_{hw2}\\ \end{matrix} \right], \left[ \begin{matrix} a_{113} & a_{123} & \cdots & a_{1w3}\\ a_{213} & a_{223} & \cdots & a_{2w3}\\ \vdots & \vdots & \ddots & \vdots\\ a_{h13} & a_{h23} & \cdots & a_{hw3}\\ \end{matrix} \right])
\Rightarrow 单张图片被卷积核覆盖的局部:( \left[ \begin{matrix} a_{111} & a_{121} & a_{131} \\ a_{211} & a_{221} & a_{231} \\ a_{311} & a_{321} & a_{331} \\ \end{matrix} \right], \left[ \begin{matrix} a_{112} & a_{122} & a_{132} \\ a_{212} & a_{222} & a_{232} \\ a_{312} & a_{322} & a_{332} \\ \end{matrix} \right], \left[ \begin{matrix} a_{113} & a_{123} & a_{133} \\ a_{213} & a_{223} & a_{233} \\ a_{313} & a_{323} & a_{333} \\ \end{matrix} \right])
\Rightarrow 局部图片转换为向量: \left[ \begin{matrix} a_{111}\\ a_{112}\\ a_{113}\\ a_{121}\\ \vdots\\ a_{333}\\ \end{matrix} \right] \Rightarrow所有图片行向量组成矩阵: \left[ \begin{matrix} a_{1111} & a_{1112} & a_{1113} & a_{1121} & \cdots & a_{1333}\\ a_{2111} & a_{2112} & a_{2113} & a_{2121} & \cdots & a_{2333}\\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots\\ a_{n111} & a_{n112} & a_{n113} & a_{n121} & \cdots & a_{n333}\\\\ \end{matrix} \right]
(2). 卷积核:( \left[ \begin{matrix} k_{111} & k_{121} & k_{131}\\ k_{211} & k_{221} & k_{231}\\ k_{311} & k_{321} & k_{331}\\ \end{matrix} \right], \left[ \begin{matrix} k_{112} & k_{122} & k_{132}\\ k_{212} & k_{222} & k_{232}\\ k_{312} & k_{322} & k_{332}\\ \end{matrix} \right], \left[ \begin{matrix} k_{113} & k_{123} & k_{133}\\ k_{213} & k_{223} & k_{233}\\ k_{313} & k_{323} & k_{333}\\ \end{matrix} \right]) \Rightarrow \left[ \begin{matrix} k_{111}\\ k_{112}\\ k_{113}\\ k_{121}\\ \vdots\\ k_{333}\\ \end{matrix} \right]
(3). (1,1)位置的输出: \left[ \begin{matrix} a_{1111} & a_{1112} & a_{1113} & a_{1121} & \cdots & a_{1333}\\ a_{2111} & a_{2112} & a_{2113} & a_{2121} & \cdots & a_{2333}\\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots\\ a_{n111} & a_{n112} & a_{n113} & a_{n121} & \cdots & a_{n333}\\\\ \end{matrix} \right] \cdot \left[ \begin{matrix} k_{111}\\ k_{112}\\ k_{113}\\ k_{121}\\ \vdots\\ k_{333}\\ \end{matrix} \right]= \left[ \begin{matrix} out_{111}\\ out_{211}\\ \vdots\\ out_{n11}\\ \end{matrix} \right]
(4). 结果的整合:( \left[ \begin{matrix} out_{111} & out_{112} & \cdots & out_{11j}\\ out_{121} & out_{122} & \cdots & out_{12j}\\ \vdots & \vdots & \ddots & \vdots\\ out_{1i1} & out_{1i2} & \cdots & out_{1ij}\\ \end{matrix} \right], \cdots, \left[ \begin{matrix} out_{n11} & out_{n12} & \cdots & out_{n1j}\\ out_{n21} & out_{n22} & \cdots & out_{n2j}\\ \vdots & \vdots & \ddots & \vdots\\ out_{ni1} & out_{ni2} & \cdots & out_{nij}\\ \end{matrix} \right])

2.基于权重矩阵的卷积运算

def conv3(array,kernel,step=1):
    #图片的张数、高、宽
    n,h,w=array.shape[:3]
    #卷积核的高、宽、颜色通道
    kh,kw,kc=kernel.shape[:3]
    #纵向和横向的移动步数
    hs,ws=(h-kh)//step+1,(w-kw)//step+1
    #滑窗卷积运算等效的权重矩阵
    weights=np.zeros((h*w*3,hs*ws))
    #纵向位置变化
    for i in range(hs):
        #卷积核的纵向覆盖范围
        start0,end0=i*step,i*step+kh
        #横向位置变化
        for j in range(ws):
            #卷积核的横向覆盖范围
            start1,end1=j*step,j*step+kw
            #当前位置的权重向量
            weights_=np.zeros((h,w,3))
            weights_[start0:end0,start1:end1]=kernel
            #合并至矩阵
            weights[:,i*ws+j]=weights_.ravel()
    #图片数据集变形
    array_=array.reshape((n,h*w*3))
    #矩阵乘法计算卷积结果
    result=np.dot(array_,weights)
    return result.reshape((-1,hs,ws)),weights

In [18]: start=time.clock()
    ...: out3,weights3=conv3(test_images,kernel)
    ...: print('\ntime used: %f s'%(time.clock()-start))

time used: 0.560485 s

In [19]: (out2==out3).all()
Out[19]: True

In [20]: weights.shape
Out[20]: (3072, 900)

In [21]: weights3.size
Out[21]: 2764800

可以看到,在消去循环体,将原本基于滑窗的卷积运算转换为权重矩阵运算后,性能得到了明显的提升(约3倍)。
但随之而来的问题就是,构造权重矩阵空间开销过大,权重矩阵形状为(输入总大小,输出总大小),即使在低分辨率的cifar数据集上,该矩阵也达到了2764800个元素的大小,随着图片分辨率的提高,开销会增大到无法接受,乃至有性能倒退的可能。
考虑到卷积运算的权重矩阵有大量的零元素,这些元素对计算没有贡献,应当采用稀疏矩阵的形式进行处理。

对基于滑窗的计算方式进行转换的思路,以起始位置为例:
(1). 卷积核:( \left[ \begin{matrix} k_{111} & k_{121} & k_{131}\\ k_{211} & k_{221} & k_{231}\\ k_{311} & k_{321} & k_{331}\\ \end{matrix} \right], \left[ \begin{matrix} k_{112} & k_{122} & k_{132}\\ k_{212} & k_{222} & k_{232}\\ k_{312} & k_{322} & k_{332}\\ \end{matrix} \right], \left[ \begin{matrix} k_{113} & k_{123} & k_{133}\\ k_{213} & k_{223} & k_{233}\\ k_{313} & k_{323} & k_{333}\\ \end{matrix} \right] )
\Rightarrow 结合位置拓展为权重矩阵:( \left[ \begin{matrix} k_{111} & k_{121} & k_{131} & \cdots & 0\\ k_{211} & k_{221} & k_{231} & \cdots & 0\\ k_{311} & k_{321} & k_{331} & \cdots & 0\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & 0 & \cdots & 0\\ \end{matrix} \right], \left[ \begin{matrix} k_{112} & k_{122} & k_{132} & \cdots & 0\\ k_{212} & k_{222} & k_{232} & \cdots & 0\\ k_{312} & k_{322} & k_{332} & \cdots & 0\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & 0 & \cdots & 0\\ \end{matrix} \right], \left[ \begin{matrix} k_{113} & k_{123} & k_{133} & \cdots & 0\\ k_{213} & k_{223} & k_{233} & \cdots & 0\\ k_{313} & k_{323} & k_{333} & \cdots & 0\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & 0 & \cdots & 0\\ \end{matrix} \right] )
\Rightarrow 展开为向量: \left[ \begin{matrix} k_{111} \\ k_{112} \\ k_{113} \\ k_{121} \\ \vdots \\ 0 \\ \end{matrix} \right]\Rightarrow 所有位置(i,j)的权重列向量堆叠: \left[ \begin{matrix} k_{11111} & 0 & 0 & \cdots & 0 \\ k_{11112} & k_{12111} & 0 & \cdots & 0 \\ k_{11113} & k_{12112} & k_{13111} & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & k_{ij333} \\ \end{matrix} \right]
(2). 第一张图片的数据:( \left[ \begin{matrix} a_{111} & a_{121} & \cdots & a_{1w1}\\ a_{211} & a_{221} & \cdots & a_{2w1}\\ \vdots & \vdots & \ddots & \vdots\\ a_{h11} & a_{h21} & \cdots & a_{hw1}\\ \end{matrix} \right] , \left[ \begin{matrix} a_{112} & a_{122} & \cdots & a_{1w2}\\ a_{212} & a_{222} & \cdots & a_{2w2}\\ \vdots & \vdots & \ddots & \vdots\\ a_{h12} & a_{h22} & \cdots & a_{hw2}\\ \end{matrix} \right], \left[ \begin{matrix} a_{113} & a_{123} & \cdots & a_{1w3}\\ a_{213} & a_{223} & \cdots & a_{2w3}\\ \vdots & \vdots & \ddots & \vdots\\ a_{h13} & a_{h23} & \cdots & a_{hw3}\\ \end{matrix} \right])
\Rightarrow 完整图片展开: \left[ \begin{matrix} a_{111}\\ a_{112}\\ a_{113}\\ a_{121}\\ \vdots\\ a_{hw3}\\ \end{matrix} \right] \Rightarrow 所有图片行向量堆叠: \left[ \begin{matrix} a_{1111}&a_{1112}&a_{1113}&a_{1121}&\cdots&a_{1hw3}\\ a_{2111}&a_{2112}&a_{2113}&a_{2121}&\cdots&a_{2hw3}\\ \vdots & \vdots & \vdots& \vdots & \ddots &\vdots \\ a_{n111}&a_{n112}&a_{n113}&a_{n121}&\cdots&a_{nhw3}\\ \end{matrix} \right]
(3). 执行矩阵乘法: \left[ \begin{matrix} a_{1111}&a_{1112}&a_{1113}&a_{1121}&\cdots&a_{1hw3}\\ a_{2111}&a_{2112}&a_{2113}&a_{2121}&\cdots&a_{2hw3}\\ \vdots & \vdots & \vdots& \vdots & \ddots &\vdots \\ a_{n111}&a_{n112}&a_{n113}&a_{n121}&\cdots&a_{nhw3}\\ \end{matrix} \right] \cdot \left[ \begin{matrix} k_{11111} & 0 & 0 & \cdots & 0 \\ k_{11112} & k_{12111} & 0 & \cdots & 0 \\ k_{11113} & k_{12112} & k_{13111} & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & k_{ij333} \\ \end{matrix} \right]
= \left[ \begin{matrix} out_{111} & out_{112} & out_{113} & \cdots & out_{1ij}\\ out_{211} & out_{212} & out_{213} & \cdots & out_{2ij}\\ out_{311} & out_{312} & out_{313} & \cdots & out_{3ij}\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ out_{n11} & out_{n12} & out_{n13} & \cdots & out_{nij}\\ \end{matrix} \right] \Rightarrow 形状还原:( \left[ \begin{matrix} out_{111} & \cdots & out_{11j} \\ \vdots & \ddots & \vdots \\ out_{1i1} & \cdots & out_{1ij} \\ \end{matrix} \right],\cdots, \left[ \begin{matrix} out_{n11} & \cdots & out_{n1j} \\ \vdots & \ddots & \vdots \\ out_{ni1} & \cdots & out_{nij} \\ \end{matrix} \right] )

3.稀疏矩阵的定义

列压缩存储

In [54]: mt=np.array([[1,2,0,0],[0,4,0,0],[0,0,0,3],[0,0,0,0]])

In [55]: mt
Out[55]: 
array([[1, 2, 0, 0],
       [0, 4, 0, 0],
       [0, 0, 0, 3],
       [0, 0, 0, 0]])

In [59]: cscm=sparse.csc_matrix(mt)

In [60]: cscm.data
Out[60]: array([1, 2, 4, 3], dtype=int32)

In [61]: cscm.indices
Out[61]: array([0, 0, 1, 2], dtype=int32)

In [62]: cscm.indptr
Out[62]: array([0, 1, 3, 3, 4], dtype=int32)

In [63]: cscm
Out[63]: 
<4x4 sparse matrix of type '<class 'numpy.int32'>'
    with 4 stored elements in Compressed Sparse Column format>

稀疏矩阵用于在矩阵中零元素占多数的情况下减少存储和运算开销,零元素越多,提升越大。
稀疏矩阵有多种压缩数据的方式,本文中因为会将稀疏矩阵用于右乘,该运算下是按列提取向量,故采用列压缩存储。
可通过传入一个稠密矩阵创建对应的稀疏矩阵,但在原矩阵构造出来会很庞大时不建议这么做,可通过直接构造稀疏矩阵的关键参数来创建。

csc_matrix有三个关键参数:
(1). data参数是存储非零元素的值的一维数组,元素按先列后行排列;
(2). indices参数是存储非零元素的列坐标的一维数组,比如一个元素处在某一列的第一个位置,对应的列坐标就是0;
(3). indptr参数是存储每列的首个非零元素在data中位置的一维数组,再额外加上data的长度;当某一列没有非零元素时,对应的indptr元素和上一列相同,如果是第一列则为0

csc_matrixtoarraytodense方法分别可以将稀疏矩阵还原为稠密矩阵的ndarray形式和matrix形式。

4.基于稀疏矩阵的卷积运算

由于先构造稠密矩阵再转换为稀疏矩阵的方式与使用稀疏矩阵减少存储开销的目的相违背,此处会通过构造关键参数直接创建稀疏矩阵。

data参数的构造较为简单,权重矩阵中的非零元素就是卷积核的元素,每个位置卷积运算的权重矩阵会被展开为列向量,csc_matrixdata的元素又是按先列后行排列的,所以只要将卷积核展开成的向量按位置总数重复拼接就行了;

indices参数的构造需要了解numpy中元素的排列顺序:
首先看起始位置,权重矩阵展开为列向量后,原本卷积核元素的位置如下所示:


图上的数字标识的即是展开为向量后元素的位置,numpy数组上对元素的访问是从最后一个轴开始的,在RGB图片形状(h,w,c)下,2轴的索引每加1,展开后的索引加1,1轴的索引每加1,展开后的索引加c,0轴索引每加1,展开后的索引加w*c
在横向移动的过程中,每移动一步,展开后索引会全体增加step*c
在纵向移动的过程中,每移动一步,展开后索引会全体增加step*w*c
因此indices可以分解为三部分:起始位置索引+横向移动带来的索引偏移+纵向移动带来的索引偏移。

indptr参数的构造也很简单,因为每列只有卷积核元素为非零元素(注意,即使卷积核元素可能为0,也要当作非零元素),所以每列首个非零元素在data中的位置可以构成以卷积核大小为步长的等差数列。

def conv4(array,kernel,step=1):
    #图片的张数、高、宽
    n,h,w,c=array.shape
    #卷积核的高、宽、颜色通道
    kh,kw,kc=kernel.shape
    #纵向和横向的移动步数
    hs,ws=(h-kh)//step+1,(w-kw)//step+1
    #1.非零值
    data=np.tile(kernel.ravel(),hs*ws)
    #2.行索引
    #(即卷积核元素在对应的权重矩阵转换成的向量中的位置)
    #(1)起始位置索引
    #由三个轴方向上的索引偏移量组合得到
    idx0=np.arange(0,kh*w*c,w*c).reshape((-1,1,1))
    idx1=np.arange(0,kw*c,c).reshape((1,-1,1))
    idx2=np.arange(0,c,1).reshape((1,1,-1))
    loc_base_=idx0+idx1+idx2
    loc_base=np.tile(loc_base_.ravel(),hs*ws)
    #(2)横向和纵向移动的索引偏移
    loc_offset0=np.arange(0,hs*step*w*c,step*w*c).reshape((-1,1))
    loc_offset1=np.arange(0,ws*step*c,step*c).reshape((1,-1))
    loc_offset_=loc_offset0+loc_offset1
    loc_offset=loc_offset_.repeat(kh*kw*kc)
    indices=loc_base+loc_offset
    #3.列偏移
    #(即每列第一个非零值在values中的位置)
    indptr=np.arange(hs*ws+1)*(kh*kw*kc)
    #构造稀疏矩阵
    weights=sparse.csc_matrix((data,indices,indptr))
    #图片数据集变形
    array_=array.reshape((n,h*w*3))
    #稀疏矩阵乘法计算卷积结果
    result=array_*weights
    return result.reshape((-1,hs,ws)),weights

In [77]: start=time.clock()
    ...: out4,weights4=conv4(test_images,kernel)
    ...: print('\ntime used: %f s'%(time.clock()-start))

time used: 0.728124 s

In [77]: (out2==out4).all()
Out[77]: True

In [86]: weights4.data.size+weights4.indices.size+weights4.indptr.size
Out[86]: 49501

可以看到,虽然性能上稍有折损,但关键的内存开销降下来了,权重矩阵从原本的输入大小*输出大小个元素,减少到卷积核大小*输出大小*2+输出大小+1个元素。

在模型训练完毕后,可将所有卷积核转换为稀疏矩阵存储,避免每次前向传播都要重新构造。

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