李理:Theano tutorial和卷积神经网络的Theano实现 Part2

本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第9篇。

作者:李理

目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。

相关文章:

李理:从Image Caption Generation理解深度学习(part I)

李理:从Image Caption Generation理解深度学习(part II)

李理:从Image Caption Generation理解深度学习(part III)

李理:自动梯度求解 反向传播算法的另外一种视角

李理:自动梯度求解——cs231n的notes

李理:自动梯度求解——使用自动求导实现多层神经网络

李理:详解卷积神经网络

李理:Theano tutorial和卷积神经网络的Theano实现 Part1

上文

7. 使用Theano实现CNN

接下来我们继续上文,阅读代码network3.py,了解怎么用Theano实现CNN。

完整的代码参考这里。

7.1 FullyConnectedLayer类

首先我们看怎么用Theano实现全连接的层。

class FullyConnectedLayer(object):? ? def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):? ? ? ? self.n_in = n_in? ? ? ? self.n_out = n_out? ? ? ? self.activation_fn = activation_fn? ? ? ? self.p_dropout = p_dropout? ? ? ? # Initialize weights and biases? ? ? ? self.w = theano.shared(? ? ? ? ? ? np.asarray(? ? ? ? ? ? ? ? np.random.normal(? ? ? ? ? ? ? ? ? ? loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),? ? ? ? ? ? ? ? dtype=theano.config.floatX),? ? ? ? ? ? name='w', borrow=True)? ? ? ? self.b = theano.shared(? ? ? ? ? ? np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),? ? ? ? ? ? ? ? ? ? ? dtype=theano.config.floatX),? ? ? ? ? ? name='b', borrow=True)? ? ? ? self.params =[self.w, self.b]? ? def set_inpt(self, inpt, inpt_dropout, mini_batch_size):? ? ? ? self.inpt = inpt.reshape((mini_batch_size, self.n_in))? ? ? ? self.output = self.activation_fn(? ? ? ? ? ? (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)? ? ? ? self.y_out = T.argmax(self.output, axis=1)? ? ? ? self.inpt_dropout = dropout_layer(? ? ? ? ? ? inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)? ? ? ? self.output_dropout = self.activation_fn(? ? ? ? ? ? T.dot(self.inpt_dropout, self.w) + self.b)? ? def accuracy(self, y):? ? ? ? "Return the accuracy for the mini-batch."? ? ? ? return T.mean(T.eq(y, self.y_out))

7.1.1 init

FullyConnectedLayer类的构造函数主要是定义共享变量w和b,并且随机初始化。参数的初始化非常重要,会影响模型的收敛速度甚至是否能收敛。这里把w和b初始化成均值0,标准差为sqrt(1.0/n_out)的随机值。有兴趣的读者可以参考这里。

此外,这里使用了np.asarray函数。我们用np.random.normal生成了(n_in, n_out)的ndarray,但是这个ndarray的dtype是float64,但是我们为了让它(可能)在GPU上运算,需要用theano.config.floatX,所以用了np.asarray函数。这个函数和np.array不同的一点是它会尽量重用传入的空间而不是深度拷贝。

另外也会把激活函数activation_fn和dropout保存到self里。activation_fn是一个函数,可能使用静态语言习惯的读者不太习惯,其实可以理解为c语言的函数指针或者函数式变成语言的lambda之类的东西。此外,init函数也把参数保存到self.params里边,这样的好处是之后把很多Layer拼成一个大的Network时所有的参数很容易通过遍历每一层的params就行。

7.1.2 set_input

set_inpt函数用来设置这一层的输入并且计算输出。这里使用了变量名为inpt而不是input的原因是input是Python的一个内置函数,容易混淆。注意我们通过两种方式设置输入:self.inpt和self.inpt_dropout。这样做的原因是我们训练的时候需要dropout。我们使用了一层dropout_layer,它会随机的把dropout比例的神经元的输出设置成0。而测试的时候我们就不需要这个dropout_layer了,但是要记得把输出乘以(1-dropout),因为我们训练的时候随机的丢弃了dropout个神经元,测试的时候没有丢弃,那么输出就会把训练的时候大,所以要乘以(1-dropout),模拟丢弃的效果?!镜比换褂幸恢謉ropout的方式是训练是把输出除以(1-dropout),这样预测的时候就不用在乘以(1-dropout)了, 感兴趣的读者可以参考这里

def set_inpt(self, inpt, inpt_dropout, mini_batch_size):

self.inpt = inpt.reshape((mini_batch_size, self.n_in))

self.output = self.activation_fn( (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)

self.y_out = T.argmax(self.output, axis=1)

self.inpt_dropout = dropout_layer(inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)

self.output_dropout = self.activation_fn( T.dot(self.inpt_dropout, self.w) + self.b)

下面我们逐行解读。

1.reshape inpt

首先把input reshape成(batch_size, n_in),为什么要reshape呢?因为我们在CNN里通常在最后一个卷积pooling层后加一个全连接层,而CNN的输出是4维的tensor(batch_size, num_filter, width, height),我们需要把它reshape成(batch_size, num_filter * width * height)。当然我们定义网络的时候就会指定n_in=num_filter width height了。否则就不对了。

2.定义output

然后我们定义self.output。这是一个仿射变换,然后要乘以(1-p_dropout),原因前面解释过了。这是预测的时候用的输入和输出。【有点读者可能会疑惑(包括我自己第一次阅读时),调用这个函数时会同时传入inpt和inpt_dropout吗?我们在Theano里只是”定义“符号变量从而定义这个计算图,所以不是真的计算。我们训练的时候定义用的是cost损失函数,它用的是inpt_dropout和output_dropout,而test的Theano函数是accuracy,用的是inpt和output以及y_out。

3.定义y_out

这个计算最终的输出,也就是当这一层作为最后一层的时候输出的分类结果。ConvPoolLayer是没有实现y_out的计算的,因为我们不会把卷积作为网络的输出层,但是全连接层是有可能作为输出的,所以通过argmax来选择最大的那一个作为输出。SoftmaxLayer是经常作为输出的,所以也实现了y_out。

4.inpt_dropout 先reshape,然后加一个dropout的op,这个op就是随机的把一些神经元的输出设置成0

def dropout_layer(layer, p_dropout):

srng = shared_randomstreams.RandomStreams(np.random.RandomState(0).randint(999999))

mask = srng.binomial(n=1, p=1-p_dropout, size=layer.shape)

return layer*T.cast(mask, theano.config.floatX)

5.定义output_dropout

直接计算

ConvPoolLayer和SoftmaxLayer的代码是类似的,这里就不赘述了。下面会有network3.py的完整代码,感兴趣的读者可以自行阅读。

但是也有一些细节值得注意。对于ConvPoolLayer和SoftmaxLayer,我们需要根据对应的公式计算输出。不过非常幸运,Theano提供了内置的op,如卷积,max-pooling,softmax函数等等。

当我们实现softmax层时,我们没有讨论怎么初始化weights和biases。之前我们讨论过sigmoid层怎么初始化参数,但是那些方法不见得就适合softmax层。这里直接初始化成0了。这看起来很随意,不过在实践中发现没有太大问题。

7.2 ConvPoolLayer类

7.2.1 init

def __init__(self, filter_shape, image_shape, poolsize=(2, 2),? ? ? ? ? ? ? ? activation_fn=sigmoid):? ? ? ? self.filter_shape = filter_shape? ? ? ? self.image_shape = image_shape? ? ? ? self.poolsize = poolsize? ? ? ? self.activation_fn=activation_fn? ? ? ? # initialize weights and biases? ? ? ? n_out = (filter_shape[0]*np.prod(filter_shape[2:])/np.prod(poolsize))? ? ? ? self.w = theano.shared(? ? ? ? ? ? np.asarray(? ? ? ? ? ? ? ? np.random.normal(loc=0, scale=np.sqrt(1.0/n_out), size=filter_shape),? ? ? ? ? ? ? ? dtype=theano.config.floatX),? ? ? ? ? ? borrow=True)? ? ? ? self.b = theano.shared(? ? ? ? ? ? np.asarray(? ? ? ? ? ? ? ? np.random.normal(loc=0, scale=1.0, size=(filter_shape[0],)),? ? ? ? ? ? ? ? dtype=theano.config.floatX),? ? ? ? ? ? borrow=True)? ? ? ? self.params =[self.w, self.b]

首先是参数。

1.filter_shape (num_filter, input_feature_map, filter_width, filter_height)

这个参数是filter的参数,第一个是这一层的filter的个数,第二个是输入特征映射的个数,第三个是filter的width,第四个是filter的height

2.image_shape(mini_batch, input_feature_map, width, height)

输入图像的参数,第一个是mini_batch大小,第二个是输入特征映射个数,必须要和filter_shape的第二个参数一样!第三个是输入图像的width,第四个是height

3.poolsize

pooling的width和height,默认2*2

4.activation_fn

激活函数,默认是sigmoid

代码除了保存这些参数之外就是定义共享变量w和b,然后保存到self.params里。

7.2.2 set_inpt

def set_inpt(self, inpt, inpt_dropout, mini_batch_size):

self.inpt = inpt.reshape(self.image_shape)

conv_out = conv.conv2d(

input=self.inpt, filters=self.w, filter_shape=self.filter_shape,

image_shape=self.image_shape)

pooled_out = downsample.max_pool_2d(

input=conv_out, ds=self.poolsize, ignore_border=True)

self.output = self.activation_fn(

pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))

self.output_dropout = self.output # no dropout in the convolutional layers

我们逐行解读

1.reshape输入

2.卷积

使用theano提供的conv2d op计算卷积

3.max-pooling

使用theano提供的max_pool_2d定义pooled_out

4.应用激活函数

值得注意的是dimshuffle函数,pooled_out是(batch_size, num_filter, out_width, out_height),b是num_filter的向量。我们需要通过broadcasting让所有的pooled_out都加上一个bias,所以我们需要用dimshuffle函数把b变成(1,num_filter, 1, 1)的tensor。dimshuffle的参数’x’表示增加一个维度,数字0表示原来这个tensor的第0维。 dimshuffle(‘x’, 0, ‘x’, ‘x’))的意思就是在原来这个vector的前面插入一个维度,后面插入两个维度,所以变成了(1,num_filter, 1, 1)的tensor。

5.output_dropout

卷积层没有dropout,所以output和output_dropout是同一个符号变量

7.3 Network类

7.3.1 init

def __init__(self, layers, mini_batch_size):? ? ? ? self.layers = layers? ? ? ? self.mini_batch_size = mini_batch_size? ? ? ? self.params =[param for layer in self.layers for param in layer.params]? ? ? ? self.x = T.matrix("x")? ? ? ? self.y = T.ivector("y")? ? ? ? init_layer = self.layers[0]? ? ? ? init_layer.set_inpt(self.x, self.x, self.mini_batch_size)? ? ? ? for j in xrange(1, len(self.layers)):? ? ? ? ? ? prev_layer, layer? = self.layers[j-1], self.layers[j]? ? ? ? ? ? layer.set_inpt(? ? ? ? ? ? ? ? prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)? ? ? ? self.output = self.layers[-1].output? ? ? ? self.output_dropout = self.layers[-1].output_dropout

参数layers就是网络的所有Layers。

比如下面的代码定义了一个三层的网络,一个卷积pooling层,一个全连接层和一个softmax输出层,输入大小是mini_batch_size 1 28 28的MNIST图片,卷积层的输出是mini_batch_size 20 24 24,pooling之后是mini_batch_size 20 12 12。然后接一个全连接层,全连接层的输入就是pooling的输出20 12*12,输出是100。最后是一个softmax,输入是100,输出10。

net = Network([ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),? ? ? ? ? ? ? ? ? ? ? filter_shape=(20, 1, 5, 5),? ? ? ? ? ? ? ? ? ? ? poolsize=(2, 2)),? ? ? ? FullyConnectedLayer(n_in=20*12*12, n_out=100),? ? ? ? SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)

首先是保存layers和mini_batch_size

self.params=[param for layer in …]这行代码把所有层的参数放到一个list里。Network.SGD方法会使用self.params来更新所以的参数。self.x=T.matrix(“x”)和self.y=T.ivector(“y”)定义Theano符号变量x和y。这代表整个网络的输入和输出。

首先我们调用init_layer的set_inpt

init_layer = self.layers[0]? init_layer.set_inpt(self.x, self.x, self.mini_batch_size)

这里调用第一层的set_inpt函数。传入的inpt和inpt_dropout都是self.x,因为不论是训练还是测试,第一层的都是x。

然后从第二层开始:

for j in xrange(1, len(self.layers)):? ? ? ? ? ? prev_layer, layer? = self.layers[j-1], self.layers[j]? ? ? ? ? ? layer.set_inpt(? ? ? ? ? ? ? ? prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)

拿到上一层prev_layer和当前层layer,然后把调用layer.set_inpt函数,把上一层的output和output_dropout作为当前层的inpt和inpt_dropout。

最后定义整个网络的output和output_dropout`

self.output = self.layers[-1].output? ? ? ? self.output_dropout = self.layers[-1].output_dropout

7.3.2 SGD函数

def SGD(self, training_data, epochs, mini_batch_size, eta,? ? ? ? ? ? validation_data, test_data, lmbda=0.0):? ? ? ? """Train the network using mini-batch stochastic gradient descent."""? ? ? ? training_x, training_y = training_data? ? ? ? validation_x, validation_y = validation_data? ? ? ? test_x, test_y = test_data? ? ? ? # compute number of minibatches for training, validation and testing? ? ? ? num_training_batches = size(training_data)/mini_batch_size? ? ? ? num_validation_batches = size(validation_data)/mini_batch_size? ? ? ? num_test_batches = size(test_data)/mini_batch_size? ? ? ? # define the (regularized) cost function, symbolic gradients, and updates? ? ? ? l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])? ? ? ? cost = self.layers[-1].cost(self)+\? ? ? ? ? ? ? 0.5*lmbda*l2_norm_squared/num_training_batches? ? ? ? grads = T.grad(cost, self.params)? ? ? ? updates =[(param, param-eta*grad)? ? ? ? ? ? ? ? ? for param, grad in zip(self.params, grads)]? ? ? ? # define functions to train a mini-batch, and to compute the? ? ? ? # accuracy in validation and test mini-batches.? ? ? ? i = T.lscalar() # mini-batch index? ? ? ? train_mb = theano.function(? ? ? ? ? ? , cost, updates=updates,? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],? ? ? ? ? ? ? ? self.y:? ? ? ? ? ? ? ? training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })? ? ? ? validate_mb_accuracy = theano.function(? ? ? ? ? ? , self.layers[-1].accuracy(self.y),? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],? ? ? ? ? ? ? ? self.y:? ? ? ? ? ? ? ? validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })? ? ? ? test_mb_accuracy = theano.function(? ? ? ? ? ? , self.layers[-1].accuracy(self.y),? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],? ? ? ? ? ? ? ? self.y:? ? ? ? ? ? ? ? test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })? ? ? ? self.test_mb_predictions = theano.function(? ? ? ? ? ? , self.layers[-1].y_out,? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })? ? ? ? # Do the actual training? ? ? ? best_validation_accuracy = 0.0? ? ? ? for epoch in xrange(epochs):? ? ? ? ? ? for minibatch_index in xrange(num_training_batches):? ? ? ? ? ? ? ? iteration = num_training_batches*epoch+minibatch_index? ? ? ? ? ? ? ? if iteration % 1000 == 0:? ? ? ? ? ? ? ? ? ? print("Training mini-batch number {0}".format(iteration))? ? ? ? ? ? ? ? cost_ij = train_mb(minibatch_index)? ? ? ? ? ? ? ? if (iteration+1) % num_training_batches == 0:? ? ? ? ? ? ? ? ? ? validation_accuracy = np.mean([validate_mb_accuracy(j) for j in xrange(num_validation_batches)])? ? ? ? ? ? ? ? ? ? print("Epoch {0}: validation accuracy {1:.2%}".format(? ? ? ? ? ? ? ? ? ? ? ? epoch, validation_accuracy))? ? ? ? ? ? ? ? ? ? if validation_accuracy >= best_validation_accuracy:? ? ? ? ? ? ? ? ? ? ? ? print("This is the best validation accuracy to date.")? ? ? ? ? ? ? ? ? ? ? ? best_validation_accuracy = validation_accuracy? ? ? ? ? ? ? ? ? ? ? ? best_iteration = iteration? ? ? ? ? ? ? ? ? ? ? ? if test_data:? ? ? ? ? ? ? ? ? ? ? ? ? ? test_accuracy = np.mean([test_mb_accuracy(j) for j in xrange(num_test_batches)])? ? ? ? ? ? ? ? ? ? ? ? ? ? print('The corresponding test accuracy is {0:.2%}'.format(? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? test_accuracy))? ? ? ? print("Finished training network.")? ? ? ? print("Best validation accuracy of {0:.2%} obtained at iteration {1}".format(? ? ? ? ? ? best_validation_accuracy, best_iteration))? ? ? ? print("Corresponding test accuracy of {0:.2%}".format(test_accuracy))

有了之前theano的基础和实现过LogisticRegression,阅读SGD应该比较轻松了。

虽然看起来代码比较多,但是其实逻辑很清楚和简单,我们下面简单的解读一下。

1. 定义损失函数cost?

l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])? ? ? ? cost = self.layers[-1].cost(self)+\? ? ? ? ? ? ? 0.5*lmbda*l2_norm_squared/num_training_batches

出来最后一层的cost,我们还需要加上L2的normalization,其实就是把所有的w平方和然后开方。注意 self.layers[-1].cost(self),传入的参数是Network对象【函数cost的第一个参数self是对象指针,不要调用者传入的,这里把Network对象自己(self)作为参数传给了cost函数的net参数】。

下面是SoftmaxLayer的cost函数:

def cost(self, net):? ? ? ? "Return the log-likelihood cost."? ? ? ? return -T.mean(T.log(self.output_dropout)[T.arange(net.y.shape[0]), net.y])

其实net只用到了net.y,我们也可以把cost定义如下:

def cost(self, y):? ? ? ? "Return the log-likelihood cost."? ? ? ? return -T.mean(T.log(self.output_dropout)[T.arange(y.shape[0]), y])

然后调用的时候用

cost = self.layers[-1].cost(self.y)+\? ? ? ? ? ? ? 0.5*lmbda*l2_norm_squared/num_training_batches

我个人觉得这样更清楚。

2. 定义梯度和updates?

grads = T.grad(cost, self.params)? ? ? ? updates =[(param, param-eta*grad)? ? ? ? ? ? ? ? ? for param, grad in zip(self.params, grads)]

3. 定义训练函数?

i = T.lscalar() # mini-batch index? ? ? ? train_mb = theano.function(? ? ? ? ? ? , cost, updates=updates,? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],? ? ? ? ? ? ? ? self.y:? ? ? ? ? ? ? ? training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })

train_mb函数的输入是i,输出是cost,batch的x和y通过givens制定,这和之前的Theano tutorial里的LogisticRegression一样的。cost函数用到的是最后一层的output_dropout,从而每一层都是走计算图的inpt_dropout->output_dropout路径。

4. 定义validation和测试函数?

validate_mb_accuracy = theano.function(? ? ? ? ? ? , self.layers[-1].accuracy(self.y),? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],? ? ? ? ? ? ? ? self.y:? ? ? ? ? ? ? ? validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })? ? ? ? test_mb_accuracy = theano.function(? ? ? ? ? ? , self.layers[-1].accuracy(self.y),? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],? ? ? ? ? ? ? ? self.y:? ? ? ? ? ? ? ? test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })

输出是最后一层的accuracy self.layers[-1].accuracy(self.y)。accuracy使用的是最后一层的output,从而每一层都是用计算图的inpt->output路径。

5. 预测函数?

self.test_mb_predictions = theano.function(? ? ? ? ? ? , self.layers[-1].y_out,? ? ? ? ? ? givens={? ? ? ? ? ? ? ? self.x:? ? ? ? ? ? ? ? test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]? ? ? ? ? ? })

输出是最后一层的y_out,也就是softmax的argmax(output)

7.4 用法?

training_data, validation_data, test_data = network3.load_data_shared()mini_batch_size = 10net = Network([ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),? ? ? ? ? ? ? ? ? ? ? filter_shape=(20, 1, 5, 5),? ? ? ? ? ? ? ? ? ? ? poolsize=(2, 2)),? ? ? ? FullyConnectedLayer(n_in=20*12*12, n_out=100),? ? ? ? SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)net.SGD(training_data, 60, mini_batch_size, 0.1,? ? ? ? ? ? validation_data, test_data)

至此,我们介绍了Theano的基础知识以及怎么用Theano实现CNN。下一讲将会介绍怎么自己用Python(numpy)实现CNN并且介绍实现的一些细节和性能优化,大部分内容来自CS231N的slides和作业assignment2,敬请关注。

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

推荐阅读更多精彩内容