【动手学深度学习】Task05

卷积神经网络基础

二维卷积层

二维互相关运算

二维互相关(cross-correlation)运算的输入是一个二维输入数组和一个二维核(kernel)数组,输出也是一个二维数组,其中核数组通常称为卷积核或过滤器(filter)。卷积核的尺寸通常小于输入数组,卷积核在输入数组上滑动,在每个位置上,卷积核与该位置处的输入子数组按元素相乘并求和,得到输出数组中相应位置的元素。

二维互相关运算.png

二维卷积层

二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏置来得到输出。卷积层的模型参数包括卷积核和标量偏置。

互相关运算与卷积运算

卷积层得名于卷积运算,但卷积层中用到的并非卷积运算而是互相关运算。我们将核数组上下翻转、左右翻转,再与输入数组做互相关运算,这一过程就是卷积运算。由于卷积层的核数组是可学习的,所以使用互相关运算与使用卷积运算并无本质区别。

特征图与感受野

二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响元素x的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做x感受野(receptive field)。

填充和步幅

填充(padding)

填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。

如果原输入的高和宽是n_hn_w,卷积核的高和宽是k_hk_w,在高的两侧一共填充p_h行,在宽的两侧一共填充p_w列,则输出形状为:

(n_h+p_h-k_h+1)\times(n_w+p_w-k_w+1)

对于高度(或宽度)为大小为2 k + 1的卷积核,令步幅为1,在高(或宽)两侧选择大小为k的填充,便可保持输入与输出尺寸相同。

步幅(stride)

在互相关运算中,卷积核在输入数组上滑动,每次滑动的行数与列数即是步幅(stride)。

一般来说,当高上步幅为s_h,宽上步幅为s_w时,输出形状为:

\lfloor(n_h+p_h-k_h+s_h)/s_h\rfloor \times \lfloor(n_w+p_w-k_w+s_w)/s_w\rfloor

多输入通道和多输出通道

多输入通道

彩色图像有RGB(红、绿、蓝)3个颜色通道。

假设输入数据的通道数为c_i,卷积核形状为k_h\times k_w,我们为每个输入通道各分配一个形状为k_h\times k_w的核数组,将c_i个互相关运算的二维输出按通道相加,得到一个二维数组作为输出。我们把c_i个核数组在通道维上连结,即得到一个形状为c_i\times k_h\times k_w的卷积核。

多输出通道

卷积层的输出也可以包含多个通道,设卷积核输入通道数和输出通道数分别为c_ic_o,高和宽分别为k_hk_w。如果希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为c_i\times k_h\times k_w的核数组,将它们在输出通道维上连结,卷积核的形状即c_o\times c_i\times k_h\times k_w

对于多输出通道卷积核的一种理解:不同的核数组提取的是不同的特征。

卷积层与全连接层的对比

1.全连接层把图像展平成一个向量,在输入图像上相邻的元素可能因为展平操作不再相邻,网络难以捕捉局部信息。而卷积层的设计,天然地具有提取局部信息的能力。

2.卷积层的参数量更少。不考虑偏置的情况下,一个形状为(c_i, c_o, h, w)的卷积核的参数量是c_i \times c_o \times h \times w,与输入图像的宽高无关。假如一个卷积层的输入和输出形状分别是(c_1, h_1, w_1)(c_2, h_2, w_2),如果要用全连接层进行连接,参数数量就是c_1 \times c_2 \times h_1 \times w_1 \times h_2 \times w_2。使用卷积层可以以较少的参数数量来处理更大的图像。

卷积层的简洁实现

我们使用Pytorch中的nn.Conv2d类来实现二维卷积层,主要关注以下几个构造函数参数:

  • in_channels (python:int) – 图像输入通道数
  • out_channels (python:int) – 输出通道数
  • kernel_size (python:int or tuple) – 卷积核大小(如果参数类型是整形就是高和宽一样,如果类型是元组就分别是高和宽,下同)
  • stride (python:int or tuple, optional) – 步长
  • padding (python:int or tuple, optional) – 每条边的填充数,填充的内容默认为0
  • bias (bool, optional) – 若为真,给输出添加一个可学习的偏置项,默认为真

forward函数的参数为一个四维张量,形状为(N, C_{in}, H_{in}, W_{in}),返回值也是一个四维张量,形状为(N, C_{out}, H_{out}, W_{out}),其中N是批量大小,C, H, W分别表示通道数、高度、宽度。

池化

池化层直接计算池化窗口内元素的最大值或者平均值,该运算也分别叫做最大池化或平均池化。

池化层也可以在输入的高和宽两侧填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的工作机制一样。

在处理多通道输入数据时,池化层对每个输入通道分别池化,但不会像卷积层那样将各通道的结果按通道相加。这意味着池化层的输出通道数与输入通道数相等

池化层的简洁实现

我们使用Pytorch中的nn.MaxPool2d实现最大池化层,关注以下构造函数参数:

  • kernel_size – 池化窗口大小
  • stride – 池化窗口的步长,默认值是池化窗口的大小。
  • padding每条边的0填充数

forward函数的参数为一个四维张量,形状为(N, C, H_{in}, W_{in}),返回值也是一个四维张量,形状为(N, C, H_{out}, W_{out}),其中N是批量大小,C, H, W分别表示通道数、高度、宽度。

平均池化层使用的是nn.AvgPool2d,使用方法与nn.MaxPool2d相同。

LeNet

LeNet模型

LeNet分为卷积层块和全连接层块两个部分。

卷积层块里的基本单位是卷积层后接平均池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的平均池化层则用来降低卷积层对位置的敏感性。

卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中,每个卷积层都使用5 \times 5的窗口,并在输出上使用sigmoid激活函数。第一个卷积层输出通道数为6,第二个卷积层输出通道数则增加到16。

全连接层块含3个全连接层。它们的输出个数分别是120、84和10,其中10为输出的类别个数。

LeNet.png

LeNet模型实现

#net
class Flatten(torch.nn.Module):  #展平操作
    def forward(self, x):
        return x.view(x.shape[0], -1)

class Reshape(torch.nn.Module): #将图像大小重定型
    def forward(self, x):
        return x.view(-1,1,28,28)      #(B x C x H x W)
    
net = torch.nn.Sequential(     #Lelet                                                  
    Reshape(),
    nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), #b*1*28*28  =>b*6*28*28
    nn.Sigmoid(),                                                       
    nn.AvgPool2d(kernel_size=2, stride=2),                              #b*6*28*28  =>b*6*14*14
    nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5),           #b*6*14*14  =>b*16*10*10
    nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),                              #b*16*10*10  => b*16*5*5
    Flatten(),                                                          #b*16*5*5   => b*400
    nn.Linear(in_features=16*5*5, out_features=120),
    nn.Sigmoid(),
    nn.Linear(120, 84),
    nn.Sigmoid(),
    nn.Linear(84, 10)
)

计算准确率的函数

#计算准确率
'''
(1). net.train()
  启用 BatchNormalization 和 Dropout,将BatchNormalization和Dropout置为True
(2). net.eval()
不启用 BatchNormalization 和 Dropout,将BatchNormalization和Dropout置为False
'''

def evaluate_accuracy(data_iter, net,device=torch.device('cpu')):
    """Evaluate accuracy of a model on the given data set."""
    acc_sum,n = torch.tensor([0],dtype=torch.float32,device=device),0
    #data_iter:测试集,acc_sum:模型预测正确的总数,n:预测时的总数目
    for X,y in data_iter:
        # If device is the GPU, copy the data to the GPU.
        X,y = X.to(device),y.to(device)
        net.eval()
        with torch.no_grad(): #该区域所涉及的计算数据不需要计算梯度,也不会进行反向传播
            y = y.long()
            acc_sum += torch.sum((torch.argmax(net(X), dim=1) == y))  #[[0.2 ,0.4 ,0.5 ,0.6 ,0.8] ,[ 0.1,0.2 ,0.4 ,0.3 ,0.1]] => [ 4 , 2 ]
            n += y.shape[0]
    return acc_sum.item()/n

训练函数

#训练函数
def train_ch5(net, train_iter, test_iter,criterion, num_epochs, batch_size, device,lr=None):
    """Train and evaluate a model with CPU or GPU."""
    print('training on', device)
    net.to(device)
    optimizer = optim.SGD(net.parameters(), lr=lr)
    for epoch in range(num_epochs):
        train_l_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
        train_acc_sum = torch.tensor([0.0],dtype=torch.float32,device=device)
        n, start = 0, time.time()
        for X, y in train_iter:
            net.train() #训练过程,启用 BatchNormalization 和 Dropout
            
            optimizer.zero_grad()
            X,y = X.to(device),y.to(device) 
            y_hat = net(X)
            loss = criterion(y_hat, y) #下边一个cell中初始化criterion为交叉熵损失函数
            loss.backward()
            optimizer.step()
            
            with torch.no_grad():
                y = y.long()
                train_l_sum += loss.float()
                train_acc_sum += (torch.sum((torch.argmax(y_hat, dim=1) == y))).float()
                n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net,device)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
              'time %.1f sec'
              % (epoch + 1, train_l_sum/n, train_acc_sum/n, test_acc,
                 time.time() - start))

卷积神经网络进阶

深度卷积神经网络(AlexNet)

AlexNet首次证明了学习到的特征可以超越⼿⼯设计的特征。

LeNet使用MNIST数据集而AlexNet使用IMAGENET数据集。

AlexNet的特征:

  1. 8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。
  2. 将LeNet中的sigmoid激活函数改成了更加简单的ReLU激活函数。
  3. 用Dropout来控制全连接层的模型复杂度。
  4. 引入数据增强,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
AlexNet.jpg

代码实现

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding ,这里最后一项没有写,默认的padding是0
            nn.ReLU(),
            nn.MaxPool2d(3, 2), # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096), #这里的256是最后输入全连层的通道数,5*5是特征图的宽和高,需要自己根据每一层推算出来
            nn.ReLU(),
            nn.Dropout(0.5),
            #由于使用CPU镜像,精简网络,若为GPU镜像可添加该层
            #nn.Linear(4096, 4096),
            #nn.ReLU(),
            #nn.Dropout(0.5),

            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )

    def forward(self, img): #注意调用全连层前要先resize,把后几个维度展平
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

使用重复元素的网络(VGG)

AlexNet的缺点:并没有提供简单的规则来指导后来的研究者如何设计新的网络,也就是说它的网络结构比较死板。而VGG可以重复使用简单的基础块。

VGG:通过重复使⽤简单的基础块来构建深度模型。

每个VGG Block:数个相同的填充为1、窗口形状为3\times 3的卷积层(保持输入的高和宽不变);接上一个步幅为2、窗口形状为2\times 2的最大池化层(让特征图高和宽减半)。

每个VGG block中卷积层个数,输入通道数,输出通道数是自己设置的,可以不一致。

VGG.jpg

代码实现

  • pytorch知识点:
    named_children():返回包含模型当前子模块的迭代器,可迭代模块名字和模块本身。
    代码示例:

    for name, module in model.named_children():
        if name in ['conv4', 'conv5']:
            print(module)
    

VGG11的简单实现

def vgg_block(num_convs, in_channels, out_channels): #卷积层个数,输入通道数,输出通道数
    blk = []
    for i in range(num_convs):
        if i == 0:
            blk.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
        else:
            blk.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
        blk.append(nn.ReLU())
    blk.append(nn.MaxPool2d(kernel_size=2, stride=2)) # 这里会使宽高减半
    return nn.Sequential(*blk)


conv_arch = ((1, 1, 64), (1, 64, 128), (2, 128, 256), (2, 256, 512), (2, 512, 512))
# 经过5个vgg_block, 宽高会减半5次, 变成 224/32 = 7
fc_features = 512 * 7 * 7 # c * w * h
fc_hidden_units = 4096 # 任意


def vgg(conv_arch, fc_features, fc_hidden_units=4096):
    net = nn.Sequential()
    # 卷积层部分
    for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):
        # 每经过一个vgg_block都会使宽高减半
        net.add_module("vgg_block_" + str(i+1), vgg_block(num_convs, in_channels, out_channels))
    # 全连接层部分
    net.add_module("fc", nn.Sequential(d2l.FlattenLayer(),
                                 nn.Linear(fc_features, fc_hidden_units),
                                 nn.ReLU(),
                                 nn.Dropout(0.5),
                                 nn.Linear(fc_hidden_units, fc_hidden_units),
                                 nn.ReLU(),
                                 nn.Dropout(0.5),
                                 nn.Linear(fc_hidden_units, 10)
                                ))
    return net
    
    
net = vgg(conv_arch, fc_features, fc_hidden_units)
X = torch.rand(1, 1, 224, 224)

# named_children获取一级子模块及其名字(named_modules会返回所有子模块,包括子模块的子模块)
for name, blk in net.named_children(): 
    X = blk(X)
    print(name, 'output shape: ', X.shape) #可输出每个VGG block及最后的全连层的输出shape

#还可用print(net)直接输出网络中各层的详细信息

网络中的网络(NiN)

NiN:串联多个由卷积层和“全连接”层构成的小⽹络来构建⼀个深层⽹络。

上一句话中带引号的“全连接层”,其实指的是NiN中1 \times 1的卷积层,使用它代替真正的全连接层(因为如果把卷积层和真正的全连接层连起来很不方便,要进行flatten操作,影响了整个的结构)。

每个NiN block中第一个卷积层参数可设置,后两个1 \times 1的卷积层参数一般是固定的。几个卷积层及最大池化层的设置都是和AlexNet一致的。

在LeNet,AlexNet,VGG里,通过全连接层来调整类别数,让输出张量的尺寸与类别数一致;NiN去掉了VGG最后的三个全连接层,而是用一个平均池化层来做代替,因此需要将最后一个NiN block的输出通道数设为与类别数一致。

NiN最后的全局平均池化层是对每个通道中所有元素求平均并直接⽤于分类。
作用:可显著减少模型参数尺寸,缓解过拟合,但会造成训练时间的增加。

NiN.png
  • 一般来说,1 \times 1卷积核可能会有如下作用:
    1. 放缩通道数:通过控制卷积核的数量达到通道数的放缩。
    2. 增加非线性。1×1卷积核的卷积过程相当于全连接层的计算过程,并且还加入了非线性激活函数,从而可以增加网络的非线性。
    3. 计算参数少(相比全连层)。

代码实现

def nin_block(in_channels, out_channels, kernel_size, stride, padding):             #kernel_size, stride, padding指的都是第一个卷积层的参数
    blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
                        nn.ReLU(),
                        nn.Conv2d(out_channels, out_channels, kernel_size=1),
                        nn.ReLU(),
                        nn.Conv2d(out_channels, out_channels, kernel_size=1),
                        nn.ReLU())
    return blk
    
    
# 已保存在d2lzh_pytorch
class GlobalAvgPool2d(nn.Module):
    # 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
    def __init__(self):
        super(GlobalAvgPool2d, self).__init__()
    def forward(self, x):
        return F.avg_pool2d(x, kernel_size=x.size()[2:]) #把kernel_size改成特征图的宽和高([2:]表示x的后两个维度,就是宽和高)(这样就是对一个通道的所有值求平均了)

net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, stride=4, padding=0),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nin_block(96, 256, kernel_size=5, stride=1, padding=2),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nin_block(256, 384, kernel_size=3, stride=1, padding=1),
    nn.MaxPool2d(kernel_size=3, stride=2), 
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, stride=1, padding=1),
    GlobalAvgPool2d(),  #这里的输出还是四维的
    # 将四维的输出转成二维的输出,其形状为(批量大小, 10)
    d2l.FlattenLayer())

GoogLeNet

GoogLeNet由Inception基础块组成。

Inception块相当于⼀个有4条线路的⼦⽹络。它通过不同窗口形状的卷积层和最⼤池化层来并⾏抽取信息,并使⽤1×1卷积层减少通道数从而降低模型复杂度(注意这里的1×1卷积层作用和之前的NiN的1×1卷积层是不同的,可以将输入通道和输出通道数值设为不一样的,起到放缩通道数的作用)。4条线路都保证了特征图的宽高不变。

最后使用torch.cat()将四条线路的输出在通道维上拼接。

可以⾃定义的超参数是每个层的输出通道数,我们以此来控制模型复杂度。

Inception块的结构.png
GoogLeNet.png

代码实现

class Inception(nn.Module):
    # c1 - c4为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):
        super(Inception, self).__init__()
        # 线路1,单1 x 1卷积层
        self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
        # 线路2,1 x 1卷积层后接3 x 3卷积层
        self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1 x 1卷积层后接5 x 5卷积层
        self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3 x 3最大池化层后接1 x 1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        return torch.cat((p1, p2, p3, p4), dim=1)  # 在通道维上连结输出
        
        
#这里用的输入shape是1*96*96
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64), #256 = 64+128+32+32
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), #480 = 128+192+96+64
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   d2l.GlobalAvgPool2d())

net = nn.Sequential(b1, b2, b3, b4, b5, 
                    d2l.FlattenLayer(), nn.Linear(1024, 10))

net = nn.Sequential(b1, b2, b3, b4, b5, d2l.FlattenLayer(), nn.Linear(1024, 10))

X = torch.rand(1, 1, 96, 96)

for blk in net.children(): 
    X = blk(X)
    print('output shape: ', X.shape)

#batchsize=128
batch_size = 16
# 如出现“out of memory”的报错信息,可减小batch_size或resize
#train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

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

推荐阅读更多精彩内容