【Note】MV-机器学习系列 之 神经网络 PyTorch

一、PyTorch 简介

1、Why PyTorch?

PyTorch 的优势是建立的神经网络是动态的,比如 RNN 变化时间长度的输出。而且深入 API,是可能看懂底层在干嘛。.
Tensorflow 的优势是在分布式训练上下了很多功夫。高度工业化使得很难看懂底层代码。

二、PyTorch 神经网络基础

1、Torch 或 Numpy

Torch 可以将 tensor 放在 GPU 中加速运算(前提是有合适的 GPU),就像 Numpy 会把 array 放在 CPU 中加速运算。而且 torch 做的和 numpy 能够很好的兼容,这样就能自由地转换 numpy array 和 torch tensor 了。

其实 torch 中 tensor 的运算和 numpy array 的如出一辙,我们就以对比的形式来看。如果想了解 torch 中其它更多有用的运算符,API就是你要去的地方

import torch
import numpy as np
# abs 绝对值计算
data = [-1, -2, 1, 2]
tensor = torch.FloatTensor(data)  # 转换成32位浮点 tensor
print(
    '\nabs',
    '\nnumpy: ', np.abs(data),          # [1 2 1 2]
    '\ntorch: ', torch.abs(tensor)      # [1 2 1 2]
)

# sin   三角函数 sin
print(
    '\nsin',
    '\nnumpy: ', np.sin(data),      # [-0.84147098 -0.90929743  0.84147098  0.90929743]
    '\ntorch: ', torch.sin(tensor)  # [-0.8415 -0.9093  0.8415  0.9093]
)

# mean  均值
print(
    '\nmean',
    '\nnumpy: ', np.mean(data),         # 0.0
    '\ntorch: ', torch.mean(tensor)     # 0.0

除了简单的计算,矩阵运算才是神经网络中最重要的部分。

import torch
import numpy as np
data = [[1, 2], [3, 4]]
tensor = torch.FloatTensor(data)

# 矩阵乘法
print('\n', np.matmul(data, data), '\n', torch.mm(tensor, tensor))

# 矩阵乘法
# numpy 和 torch 对于 dot 的不同之处
data = np.array(data)
print('\n', data.dot(data))
# 错误写法,因为新版本的 tensor.dot() 只能针对于一位数组,即转换成向量乘法
# print('\n', tensor.dot(tensor))
print(torch.tensordot(tensor, tensor))

# 对应元素相乘  x^2
data = np.array(data)
print('\n', data * data)
print('\n', tensor * tensor)
2、变量 Variable

在 Torch 中的 Variable 就是一个存放会变化的值的地理位置,里面的值会不停的变化。就像一个裝鸡蛋的篮子,鸡蛋数会不停变动。那谁是里面的鸡蛋呢,自然就是 Torch 的 Tensor 咯。如果用一个 Variable 进行计算,那返回的也是一个同类型的 Variable。

import torch
from torch.autograd import Variable
tensor = torch.FloatTensor([[1, 2], [3, 4]])
# requires_grad 是参与不参与误差反向传播,要不要计算梯度
var = Variable(tensor, requires_grad = True)
print(tensor)
print(var)
t_out = torch.mean(tensor * tensor)   # x^2
v_out = torch.mean(var * var)            # x^2
print(t_out)
print(v_out)

到目前为止我们看不出什么不同,但是时刻记住,Variable 计算时,它在背景幕布后面一步步默默地搭建着一个庞大的系统,叫做计算图 computational graph。这个图是用来干嘛的? 原来是将所有的计算步骤 (节点) 都连接起来,最后进行误差反向传递的时候,一次性将所有 variable 里面的修改幅度 (梯度) 都计算出来,而 tensor 就没有这个能力啦。

v_out = torch.mean(variable*variable) 就是在计算图中添加的一个计算步骤,计算误差反向传递的时候有他一份功劳,我们就来举个例子:

v_out.backward()  # 模拟 v_out 的误差反向传递
# 下面两步看不懂没关系, 只要知道 Variable 是计算图的一部分, 可以用来传递误差就好.
# v_out = 1/4 * sum(variable*variable) 这是计算图中的 v_out 计算步骤
# 针对于 v_out 的梯度就是, d(v_out)/d(variable) = 1/4*2*variable = variable/2
print(var.grad)    # 初始 Variable 的梯度

那如何获取 Variable 里面的数据呢?直接print(variable)只会输出 Variable 形式的数据,在很多时候是用不了的(比如想要用 plt 画图),所以我们要转换一下, 将它变成 tensor 形式:

print(variable)     #  Variable 形式
print(variable.data)    # tensor 形式
print(variable.data.numpy())    # numpy 形式
3、什么是激励函数 Activation Function

为什么要使用激励函数?
因为激励函数可以解决日常生活中不能用线性方程 linear function 所概括的问题。

比如说我们可以把神经网络简化为 Y = WX,但是这样只能描述一个线性问题,于是就需要激励函数来将这条直线“掰弯”,即 Y = AF(WX)。

AF 其实就是激励函数,常有的选择是 relu sigmoid tanh,把这些掰弯利器嵌套在原有的结果就可以强行把原先的线性结果扭曲成曲线了。同时我们也可以自己创造激励函数,不过要求是必须可微分,因为在 back propagation 误差反向传递时,只有可微分的激励函数才能把误差传递回去。

恰当地使用激励函数,是有窍门的。

  • 少量层结构中,对于隐藏层使用任意的激励函数不会有很大的影响。一般地,在 CNN 的卷积层中推荐 Relu,在 RNN 中推荐 tanh 或 Relu。(具体怎么选,看之后 RNN 中详细介绍)
  • 多层神经网络中,掰弯利器不能随意选择,因为会涉及到梯度爆炸梯度消失的问题。
4、激励函数 Activation

什么是 Activation
一句话概括,就是神经网络可以描述非线性问题的步骤,让神经网络变得更强大。

Torch 中的激励函数
Torch 中的激励函数有很多,常用的有relu``sigmoid``tanh``softplus

import torch
import torch.nn.functional as F
from torch.autograd import Variable
import matplotlib.pyplot as plt
x = torch.linspace(-5, 5, 200)
x = Variable(x)
x_np = x.data.numpy()  # 换成 numpy array,绘图时用
# 几种常用的 激励函数
y_relu = F.relu(x).data.numpy()
# y_sigmoid = F.sigmoid(x).data.numpy()  提示 nn.functional.sigmoid 已弃用
y_sigmoid = torch.sigmoid(x).data.numpy()
# y_tanh = F.tanh(x).data.numpy()    提示 nn.functional.tanh 已弃用
y_tanh = torch.tanh(x).data.numpy()
y_softplus = F.softplus(x).data.numpy()
# y_softmax = F.softmax(x)  softmax 比较特殊,不能直接显示,不过它是关于概率的,用于分类
plt.figure(1, figsize=(8, 6))
plt.subplot(221)
plt.plot(x_np, y_relu, c='red', label='relu')
plt.ylim((-1, 5))
plt.legend(loc='best')

plt.subplot(222)
plt.plot(x_np, y_sigmoid, c='red', label='sigmoid')
plt.ylim((-0.2, 1.2))
plt.legend(loc='best')

plt.subplot(223)
plt.plot(x_np, y_tanh, c='red', label='tanh')
plt.ylim((-1.2, 1.2))
plt.legend(loc='best')

plt.subplot(224)
plt.plot(x_np, y_softplus, c='red', label='softplus')
plt.ylim((-0.2, 6))
plt.legend(loc='best')

plt.show()

三、建造第一个神经网络

1、关系拟合 (回归)
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim = 1)
y = x.pow(2) + 0.2*torch.rand(x.size())

# 建立神经网络
# 先定义所有的层属性 __init__(),再一层层搭建层与层的关系链接 forward(x)
class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden, n_output):
        super(Net, self).__init__()   # 继承 __init__ 功能
        self.hidden = torch.nn.Linear(n_feature, n_hidden)  # 隐藏层线性输出
        self.predict = torch.nn.Linear(n_hidden, n_output)  # 输出层线性输出
    def forward(self, x):      # 这同时也是 Module 中的 forward 功能
        # 正向传播输入值,神经网络分析输出值
        x = F.relu(self.hidden(x))   # 激励函数(隐藏层的线性值)
        x = self.predict(x)          # 输出值
        return x

net = Net(n_feature=1, n_hidden=10, n_output=1)
print(net)   # 输出 net 的结构

# 训练网络
optimizer = torch.optim.SGD(net.parameters(), lr = 0.2)  # 传入 net 的所有参数,学习率
loss_func = torch.nn.MSELoss()

'''
for t in range(100):
    prediction = net(x)
    loss = loss_func(prediction, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
'''

# 可视化训练过程
plt.ion()
for t in range(200):
    prediction = net(x)
    loss = loss_func(prediction, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if t%5==0:
        plt.cla()
        plt.scatter(x.data.numpy(), y.data.numpy())
        plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw = 5)
        plt.text(0.5, 0, 'Lpss=%.4f'%loss.data.numpy(), fontdict={'size':20, 'color':'red'})
        plt.pause(0.1)
plt.ioff()
plt.show()

笔记:隐藏层数量设置巨大时,应该调小学习率,增加迭代次数。并且当隐藏层数量设置过大时,会发生震荡...比如,n_hidden = 100, lr = 0.05。

2、区分类型 (分类)
import torch
import matplotlib.pyplot as plt
import torch.nn.functional as F
# 构造假数据
n_data = torch.ones(100, 2)
x0 = torch.normal(2*n_data, 1)
y0 = torch.zeros(100)
x1 = torch.normal(-2*n_data, 1)
y1 = torch.ones(100)
# 注意 x y 的数据形式一定要像下面一样,torch.cat 是在合并数据
x = torch.cat((x0, x1), 0).type(torch.FloatTensor)
y = torch.cat((y0, y1), ).type(torch.LongTensor)

class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden, n_output):
        super(Net, self).__init__()
        self.hidden = torch.nn.Linear(n_feature, n_hidden)
        self.output = torch.nn.Linear(n_hidden, n_output)
    def forward(self, x):
        x = F.relu(self.hidden(x))
        x = self.output(x)
        return x
net = Net(2, 10, 2)  # 几个类别就有几个 output
print(net)

optimizer = torch.optim.SGD(net.parameters(), lr = 0.02)
loss_func = torch.nn.CrossEntropyLoss()
plt.ion()
plt.show()
for t in range(100):
    output = net(x)
    loss = loss_func(output, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if t%2==0:
        plt.cla()
        # 过了一道 softmax 的激励函数后的最大概率才是预测值
        prediction = torch.max(F.softmax(output), 1)[1]
        pred_y = prediction.data.numpy().squeeze()
        target_y = y.data.numpy()
        plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=pred_y, s = 100, lw=0, cmap = 'RdYlGn')
        accuracy = sum(pred_y==target_y)/200.
        plt.text(1.5, -4, 'Accuracy=%.2f'%accuracy, fontdict={'size': 20, 'color':  'red'})
        plt.pause(0.1)
plt.ioff()
plt.show()
3、快速搭建法

Torch 中提供了很多方便的途径,同样是神经网络,能快则快,我们看看如何用更简单的方式搭建同样的回归神经网络。

import torch.nn.functional as F
import torch

# 用 class 继承一个 torch 的神经网络结构,然后对其进行修改
class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden, n_output):
        super(Net, self).__init__()
        self.hidden = torch.nn.Linear(n_feature, n_hidden)
        self.output = torch.nn.Linear(n_hidden, n_output)
    def forward(self, x):
        x = F.relu(self.hidden(x))
        x = self.output(x)
        return x
net1 = Net(1, 10, 1)
print(net1)

# 快速搭建法
net2 = torch.nn.Sequential(
    torch.nn.Linear(1, 10),
    torch.nn.ReLU(),
    torch.nn.Linear(10, 1)
)
print(net2)

发现net2多显示了一些内容,这是为什么呢? 原来把激励函数也一同纳入进去了,但是net1中,激励函数实际上是在 forward()功能中才被调用的。这也就说明了,相比net2net1 的好处就是,可以根据个人需要更加个性化前向传播过程,比如(RNN)。不过如果不需要七七八八的过程,相信 net2 这种形式更适合。

4、保存提取
import torch
import matplotlib.pyplot as plt
torch.manual_seed(2) # reproducible。为 cpu 设置随机种子,保证每次产生的随机数相同
# 假数据
x = torch.unsqueeze(torch.linspace(-1, 1, 100), dim = 1)
y = x.pow(2) + 0.2*torch.rand(x.size())  # noisy y data (tensor)
# 保存网络
def save():
    net1 = torch.nn.Sequential(
        torch.nn.Linear(1, 10),
        torch.nn.ReLU(),
        torch.nn.Linear(10, 1)
    )
    optimizer = torch.optim.SGD(net1.parameters(), lr=0.5)
    loss_func = torch.nn.MSELoss()

    for t in range(100):
        prediction = net1(x)
        loss = loss_func(prediction, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # 两种保存方式,上面保存整个网络,下面只保存网络中的参数(速度快,占内存少)
    torch.save(net1, 'net.pkl')
    torch.save(net1.state_dict(), 'net_params.pkl')
    # plot result
    plt.figure(1, figsize=(10, 3))
    plt.subplot(131)
    plt.title('Net1')
    plt.scatter(x.data.numpy(), y.data.numpy())
    plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)

# 提取整个网络
def restore_net():
    net2 = torch.load('net.pkl')
    prediction = net2(x)
    # plot result
    plt.subplot(132)
    plt.title('Net2')
    plt.scatter(x.data.numpy(), y.data.numpy())
    plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
# 只提取网络参数
def resotre_params():
    # 新建 net3 结构
    net3 = torch.nn.Sequential(
        torch.nn.Linear(1, 10),
        torch.nn.ReLU(),
        torch.nn.Linear(10, 1)
    )
    # 将保存的参数复制到 net3
    net3.load_state_dict(torch.load('net_params.pkl'))
    prediction = net3(x)
    # plot result
    plt.subplot(133)
    plt.title('Net3')
    plt.scatter(x.data.numpy(), y.data.numpy())
    plt.plot(x.data.numpy(), prediction.data.numpy(), 'r-', lw=5)
    plt.show()

save()
restore_net()
resotre_params()
5、批训练 Mini-batch Training

Torch 中提供了一种帮你整理你的数据结构的好东西, 叫做 DataLoader, 我们能用它来包装自己的数据, 进行批训练.

import torch
import torch.utils.data as Data
torch.manual_seed(1)    # reproducible

BATCH_SIZE = 5      # 批训练的数据个数

x = torch.linspace(1, 10, 10)       # x data (torch tensor)
y = torch.linspace(10, 1, 10)       # y data (torch tensor)

# 先转换成 torch 能识别的 Dataset
torch_dataset = Data.TensorDataset(data_tensor=x, target_tensor=y)

# 把 dataset 放入 DataLoader
loader = Data.DataLoader(
    dataset=torch_dataset,      # torch TensorDataset format
    batch_size=BATCH_SIZE,      # mini batch size
    shuffle=True,               # 要不要打乱数据 (打乱比较好)
    num_workers=2,              # 多线程来读数据
)

for epoch in range(3):   # 训练所有!整套!数据 3 次
    for step, (batch_x, batch_y) in enumerate(loader):  # 每一步 loader 释放一小批数据用来学习
        # 假设这里就是你训练的地方...

        # 打出来一些数据
        print('Epoch: ', epoch, '| Step: ', step, '| batch x: ',
              batch_x.numpy(), '| batch y: ', batch_y.numpy())

"""
Epoch:  0 | Step:  0 | batch x:  [ 6.  7.  2.  3.  1.] | batch y:  [  5.   4.   9.   8.  10.]
Epoch:  0 | Step:  1 | batch x:  [  9.  10.   4.   8.   5.] | batch y:  [ 2.  1.  7.  3.  6.]
Epoch:  1 | Step:  0 | batch x:  [  3.   4.   2.   9.  10.] | batch y:  [ 8.  7.  9.  2.  1.]
Epoch:  1 | Step:  1 | batch x:  [ 1.  7.  8.  5.  6.] | batch y:  [ 10.   4.   3.   6.   5.]
Epoch:  2 | Step:  0 | batch x:  [ 3.  9.  2.  6.  7.] | batch y:  [ 8.  2.  9.  5.  4.]
Epoch:  2 | Step:  1 | batch x:  [ 10.   4.   8.   1.   5.] | batch y:  [  1.   7.   3.  10.   6.]
"""
6、加速神经网络训练 Speed Up Training

英文学习资料

越复杂的神经网络 , 越多的数据 , 我们需要在训练神经网络的过程上花费的时间也就越多. 原因很简单, 就是因为计算量太大了. 可是往往有时候为了解决复杂的问题, 复杂的结构和大数据又是不能避免的, 所以我们需要寻找一些方法, 让神经网络聪明起来, 快起来.

Stochastic Gradient Descent (SGD)
Momentum 更新方法
AdaGrad 更新方法
RMSprop 更新方法
Adam 更新方法

7、优化器 Optimizer
import torch
import torch.nn.functional as F
import torch.utils.data as Data
import matplotlib.pyplot as plt

torch.manual_seed(2)

LR = 0.01
BATCH_SIZE = 32
EPOCH = 12

x = torch.unsqueeze(torch.linspace(-1, 1, 1000), dim = 1)
y = x.pow(2) + 0.1*torch.normal(torch.zeros(x.size()))

torch_dataset = Data.TensorDataset(x, y)
loader = Data.DataLoader(
    dataset= torch_dataset,
    batch_size= BATCH_SIZE,
    shuffle= True,
    num_workers= 2
)

class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.hidden = torch.nn.Linear(1, 20)
        self.predict = torch.nn.Linear(20, 1)
    def forward(self, x):
        x = F.relu(self.hidden(x))
        x = self.predict(x)
        return x

if __name__ == '__main__':
    # 不实用 Adagrad 是因为它在学习率上做文章
    net_SGD = Net()
    net_Momentum = Net()
    net_RMSprop = Net()
    net_Adam = Net()
    nets = [net_SGD, net_Momentum, net_RMSprop, net_Adam]

    opt_SGD = torch.optim.SGD(net_SGD.parameters(), lr=LR)
    opt_Momentum = torch.optim.SGD(net_Momentum.parameters(), lr=LR, momentum=0.8)
    opt_RMSprop = torch.optim.RMSprop(net_RMSprop.parameters(), lr=LR, alpha=0.9)
    opt_Adam = torch.optim.Adam(net_Adam.parameters(), lr=LR, betas=(0.9, 0.99))
    optimizers = [opt_SGD, opt_Momentum, opt_RMSprop, opt_Adam]

    loss_func = torch.nn.MSELoss()
    losses_his = [[], [], [], []]

    for epoch in range(EPOCH):
        print('Epoch: ', epoch)
        for step, (batch_x, batch_y) in enumerate(loader):
            for net, opt, l_his in zip(nets, optimizers, losses_his):
                output = net(batch_x)
                loss = loss_func(output, batch_y)
                opt.zero_grad()
                loss.backward()
                opt.step()
                l_his.append(loss.data.numpy())

    labels = ['SGD', 'Momentum', 'RMSprop', 'Adam']
    for i, l_his in enumerate(losses_his):
        plt.plot(l_his, label = labels[i])
    plt.legend(loc='best')
    plt.xlabel('Steps')
    plt.ylabel('Loss')
    plt.ylim((0, 0.2))
    plt.show()


SGD 是最普通的优化器, 也可以说没有加速效果, 而 Momentum 是 SGD 的改良版, 它加入了动量原则. 后面的 RMSprop 又是 Momentum 的升级版. 而 Adam 又是 RMSprop 的升级版. 不过从这个结果中我们看到, Adam 的效果似乎比 RMSprop 要差一点. 所以说并不是越先进的优化器, 结果越佳. 我们在自己的试验中可以尝试不同的优化器, 找到那个最适合你数据/网络的优化器.

四、高级神经网络结构

1、什么是卷积神经网络 CNN

CNN 适合用于处理 图像识别、语音识别 等的任务。

2、CNN

(待填坑 ing)

3、什么是循环神经网络 RNN

加了记忆功能,适合用于 语言分析,序列化数据 的任务。

4、什么是 LSTM 循环神经网络

LSTM 网络是在,RNN 的结构上加入了三个控制:输入控制,忘记控制,输出控制。其中忘记控制会控制将过去很久的记忆按比例更新成最新记忆。

5、RNN (分类)
6、RNN (回归)
7、什么是自编码 Autoencoder
8、Autoencoder 自编码/非监督学习
9、什么是 DQN
10、DQN 强化学习
11、什么是生成对抗网络 GAN
12、GAN

五、高阶内容

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

推荐阅读更多精彩内容