基于机器学习技术实现一个医学辅助诊断的专家系统原型

写在前面

本项目由孟宁老师指导,全班68名同学共同完成,是真正的团队合作的产物(此处应有掌声)。其中我做了一些非常微小的工作,贡献了10 commits,讲了一个Online Learning算法。


commit记录

除去一些修修补补,主要是:
#87 判断请求的图片是否为目标图片
#155 将训练数据封装成对像 可以获得性别0-1矩阵、26项指标矩阵,提供方法可以得到1800项中随机的n项数据(已合并)
#229 TensorBoard可视化(已合并)
#252 将pHash.py集成到现有代码中判断图片是否为血常规报告(未处理)
【12】Online Learning - Bandit Algorithms

下面将按照课程的三个小部分来写。

A1:神经网络实现手写识别系统

这个项目可以说是深度学习的hello world。虽然代码都是现成的,但是在跑的过程中还是遇到了一些麻烦。因为原来我的电脑中装的是python3.5,和pyhton2.7中的有些包是不兼容的。所以后来又重新装了pyhton2.7总算跑起来了。

当然跑不是最重要的,重要的是理解算法。这是一个最简单的神经网络,只有一层输入层,一层隐藏层,一个输出层。输入是20乘20,也就是400个像素点,400行一列的矩阵。着色的部分为1,否则为0。输出是一个10行一列的one-hot矩阵代表是哪个数字。采用反向传播算法,它通过计算误差率然后系统根据误差改变网络的权值矩阵和偏置向量来进行训练。

初次之外,我还对一个完整的项目的运作有了了解。数据怎样从前端流向后端,再调用算法进行计算,最后显示在网页上。

  用户接口(ocr.html)--html网页

  客户端(ocr.js)--处理在客户端接收到的响应、传递服务器的响应

  服务器(server.py)--由Python标准库BaseHTTPServer实现,接收从客户端发来的训练或是预测请求,使用POST报文

  神经网络(ocr.py) --实现具体算法

  神经网络设计脚本(neural_network_design.py)--测试用

A2:血常规检验报告的图像OCR识别

这部分的代码打包到了 BloodTestReportOCR文件夹中:

 view.py --Web 端上传图片到服务器,存入mongodb并获取oid。
 imageFilter.py --对图像透视裁剪和OCR进行了简单的封装,以便于模块间的交互,规定适当的接口.是整个ocr中最重要的模块.
 classifier.py --用于判定裁剪矫正后的报告和裁剪出检测项目的编号
 imgproc.py --将识别的图像进行处理二值化等操作,提高识别率 包括对中文和数字的处理

ocr原理以及code review

ocr主要使用了opencv2包。
1 对输入的图像进行处理,采用Canny算子描绘边缘

img_sp = self.img.shape
ref_lenth = img_sp[0] * img_sp[1] * ref_lenth_multiplier
img_gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
img_gb = cv2.GaussianBlur(img_gray, (gb_param, gb_param), 0)
closed = cv2.morphologyEx(img_gb, cv2.MORPH_CLOSE, kernel)
opened = cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel)
edges = cv2.Canny(opened, canny_param_lower , canny_param_upper)
图片.png

2 调用CV2模块的findContours提取矩形轮廓,筛选对角线大于阈值的轮廓

contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

def getbox(i):
rect = cv2.minAreaRect(contours[i])
box = cv2.cv.BoxPoints(rect)
box = np.int0(box)
return box

def distance(box):
delta1 = box[0]-box[2]
delta2 = box[1]-box[3]
distance1 = np.dot(delta1,delta1)
distance2 = np.dot(delta2,delta2)
distance_avg = (distance1 + distance2) / 2
return distance_avg

# 筛选出对角线足够大的几个轮廓
found = []
for i in range(len(contours)):
box = getbox(i)
distance_arr = distance(box)
if distance_arr > ref_lenth:
found.append([i, box])

def getline(box):
if np.dot(box[1]-box[2],box[1]-box[2]) < np.dot(box[0]-box[1],box[0]-box[1]):
point1 = (box[1] + box[2]) / 2
point2 = (box[3] + box[0]) / 2
lenth = np.dot(point1-point2, point1-point2)
return point1, point2, lenth
else:
point1 = (box[0] + box[1]) / 2
point2 = (box[2] + box[3]) / 2
lenth = np.dot(point1-point2, point1-point2)
return point1, point2, lenth

def cmp(p1, p2):
delta = p1 - p2
distance = np.dot(delta, delta)
if distance < img_sp[0] * img_sp[1] * ref_close_multiplier:
return 1
else:
return 0

def linecmp(l1, l2):
f_point1 = l1[0]
f_point2 = l1[1]
f_lenth = l1[2]
b_point1 = l2[0]
b_point2 = l2[1]
b_lenth = l2[2]
if cmp(f_point1,b_point1) or cmp(f_point1,b_point2) or cmp(f_point2,b_point1) or cmp(f_point2,b_point2):
if f_lenth > b_lenth:
return 1
else:
return -1
else:
return 0

def deleteline(line, j):
lenth = len(line)
for i in range(lenth):
if line[i] is j:
del line[i]
return
筛选对角线大于阈值的矩形

3 将轮廓变成线,并去除不合适的线

# 比较最小外接矩形相邻两条边的长短,以两条短边的中点作为线的两端
line = []

for i in found:
box = i[1]
point1, point2, lenth = getline(box)
line.append([point1, point2, lenth])

# 把不合适的线删去
if len(line)>3:
for i in line:
for j in line:
if i is not j:
rst = linecmp(i, j)
if rst > 0:
deleteline(line, j)
elif rst < 0:
deleteline(line, i)

#检测出的线数量不对就返回-1跳出
if len(line) != 3:
print "it is not a is Report!,len(line) =",len(line)
return None

def distance_line(i, j):
dis1 = np.dot(i[0]-j[0], i[0]-j[0])
dis2 = np.dot(i[0]-j[1], i[0]-j[1])
dis3 = np.dot(i[1]-j[0], i[1]-j[0])
dis4 = np.dot(i[1]-j[1], i[1]-j[1])
return min(dis1, dis2, dis3, dis4)

def findhead(i, j, k):
dis = []
dis.append([distance_line(i, j), i, j])
dis.append([distance_line(j, k), j, k])
dis.append([distance_line(k, i), k, i])
dis.sort()
if dis[0][1] is dis[2][2]:
return dis[0][1], dis[2][1]
if dis[0][2] is dis[2][1]:
return dis[0][2], dis[2][2]

def cross(vector1, vector2):
return vector1[0]*vector2[1]-vector1[1]*vector2[0]

由三条粗线确定报告区域

4 使用透视变换将表格区域转换为一个1000*760的图,得到可用于ocr剪切的照片
5 autocut(self, num, param=default)函数用于剪切ImageFilter中的img成员,剪切之前调用filter(param)判断是否为可识别的图像,剪切之后临时图片保存在out_path,如果剪切失败,返回-1,成功返回0
6 ocr(self, num)函数用于对img进行ocr识别,返回一个json格式数据。
ocr流程图

图片相似度的判断

在这部分我的主要贡献是pHash.py来判断用户上传的图片是否是我们可处理的图片,比如用户上传了一张猫的图片,或者就是学常规报告,但质量太差我们没法处理,需要及时给用户反馈,重新拍照上传。
我的思路是和已有的一张已经裁剪好的标准图片做对比,如果相似度在一定范围内我们就认为这个图片是合格的。这和购物网站或者搜索引擎中目前“以图搜图”所使用的算法本质上是一样的。对算法原理的说明如下:

第一步,缩小尺寸。
最快速的去除高频和细节,只保留结构明暗的方法就是缩小尺寸。将图片缩小到8x8的尺寸,总共64个像素。摒弃不同尺寸、比例带来的图片差异。
第二步,简化色彩。
将缩小后的图片,转为64级灰度。也就是说,所有像素点总共只有64种颜色。
第三步,计算DCT(离散余弦变换)。
DCT是把图片分解频率聚集和梯状形,虽然JPEG使用8*8的DCT变换,在这里使用32*32的DCT变换。
第四步,缩小DCT。
虽然DCT的结果是32*32大小的矩阵,但我们只要保留左上角的8*8的矩阵,这部分呈现了图片中的最低频率。
第五步,计算平均值。
计算所有64个值的平均值。
第六步,进一步减小DCT。
根据8*8的DCT矩阵,设置0或1的64位的hash值,大于等于DCT均值的设为”1”,小于DCT均值的设为“0”。

关于DCT:
离散余弦变换(DCT)获取图片的低频成分。
离散余弦变换(DCT)是种图像压缩算法,它将图像从像素域变换到频率域。然后一般图像都存在很多冗余和相关性的,所以转换到频率域之后,只有很少的一部分频率分量的系数才不为0,大部分系数都为0(或者说接近于0)。从图片左上角依次到右下角,频率越来越高,由图可以看到,左上角的值比较大,到右下角的值就很小很小了。换句话说,图像的能量几乎都集中在左上角这个地方的低频系数上面了。

原图与其频率

A3:根据血常规检验的各项数据预测年龄和性别

下面我们要做的就是构建机器学习模型了。我们使用了2000多份血常规报告,90%用于训练,10%用于检验。我使用的是基于tensorflow实现的神经网络

神经网络原理

神经网络由能够互相通信的节点构成,赫布理论解释了人体的神经网络是如何通过改变自身的结构和神经连接的强度来记忆某种模式的。而人工智能中的神经网络与此类似。请看下图,最左一列蓝色节点是输入节点,最右列节点是输出节点,中间节点是隐藏节点。该图结构是分层的,隐藏的部分有时候也会分为多个隐藏层。如果使用的层数非常多就会变成我们平常说的深度学习了。


一个简单的神经网络

每一层(除了输入层)的节点由前一层的节点加权加相加加偏置向量并经过激活函数得到,公式如下:


f是激活函数,b是偏置向量

神经网络属于监督学习,那么多半就三件事,决定模型参数,通过数据集训练学习,训练好后就能到分类工具/识别系统用了。数据集可以分为2部分(训练集,验证集),也可以分为3部分(训练集,验证集,测试集),训练集可以看作平时做的习题集(可反复做)。通过不断的训练减少损失,我们就可以得到最优的参数,即偏置向量和权重。

调参经验

在总结点数量差不多的情况下,深层但每层的隐藏节点数较少的网络较之浅层但每层节点数的网络效果要好。其它参数,近两年论文基本都用同样的参数设定:迭代几十到几百epoch。sgd,mini batch size从几十到几百皆可。步长0.1,可手动收缩,weight decay取0.005,momentum取0.9。dropout加relu。weight用高斯分布初始化,bias全初始化为0。输入特征和预测目标都做好归一化也有助于提高准确率。
在A2中,因为我写的神经网络和版本库上的类似,所以就没有上传,下面讲讲我的贡献,数据封装和可视化

数据封装

因为看到minist手写识别的代码中把数据集封装成了对象,用起来很方便,所以就做了这个。

train = Traindata()                  #初始化
gender = train.gender                #性别的one-hot矩阵
age = train.age                      #年龄
para = train.parameter               #26项指标矩阵
train.next_batch_gender(n)           #随机抽取n项数据对应的指标及其性别
train.next_batch_age(n)              #同上

TensorBoard:可视化学习

官方文档中的介绍是这样的:

TensorBoard 涉及到的运算,通常是在训练庞大的深度神经网络中出现的复杂而又难以理解的运算。
为了更方便 TensorFlow 程序的理解、调试与优化,我们发布了一套叫做 TensorBoard 的可视化工具。你可以用 TensorBoard 来展现你的 TensorFlow 图像,绘制图像生成的定量指标图以及附加数据。

想要可视化,首先要定义图层和对节点命名:
使用with tf.name_scope('inputs')xsys包含进来,形成一个大的图层,图层的名字就是with tf.name_scope()方法里的参数。

with tf.name_scope('inputs'): 
# define placeholder for inputs to network 
xs = tf.placeholder(tf.float32, [None, 1]) 
ys = tf.placeholder(tf.float32, [None, 1])

然后再次对ys指定名称y_inxs同理:

ys= tf.placeholder(tf.loat32, [None, 1],name='y_in')

在定义完大的框架layer之后,也可以定义每一个’框架‘里面的小部件:(Weights biases 和 activation function)。如对 Weights 定义: 定义的方法同上,可以使用tf.name.scope()方法,同时也可以在Weights中指定名称W。 即为:

def add_layer(inputs, in_size, out_size, activation_function=None): 
#define layer name 
with tf.name_scope('layer'): 
#define weights name  
    with tf.name_scope('weights'): 
    Weights= tf.Variable(tf.random_normal([in_size, out_size]),name='W') 
#and so on......

接下来,我们为层中的Weights设置变化图, tensorflow中提供了tf.histogram_summary()方法,用来绘制图片, 第一个参数是图表的名称, 第二个参数是图表要记录的变量

tf.histogram_summary(layer_name+'/weights',Weights)

Loss 的变化图和之前设置的方法略有不同. loss是在tesnorBorad 的event下面的, 这是由于我们使用的是tf.scalar_summary() 方法

with tf.name_scope('loss'): 
    loss= tf.reduce_mean(tf.reduce_sum( tf.square(ys- prediction), reduction_indices=[1])) 
    tf.scalar_summary('loss',loss)

接下来, 开始合并打包,tf.merge_all_summaries() 方法会对我们所有的 summaries合并到一起. 因此在原有代码片段中添加:

sess= tf.Session()
merged= tf.merge_all_summaries()
# tf.train.SummaryWriter soon be deprecated, use following
writer = tf.summary.FileWriter("logs/", sess.graph)
sess.run(tf.initialize_all_variables())

程序运行完毕之后, 会产生logs目录 , 使用命令 tesnsorboard --logdir='logs/',打开终端中输出的URL地址即可。

运行
参数分布图

代码

以年龄预测为例:

# -*- coding: utf-8 -*-

import tensorflow as tf
import numpy as np
import csv
import math

label_orign2 = []
data_orign2 = []
sex_orign2 = []
age_orign2 = []

#读预测数据
with open('predict.csv','rb') as precsv2:
reader2 = csv.reader(precsv2)
for line2 in reader2:

if reader2.line_num == 1:
continue 
label_origntemp2 = [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0] #升维度
label_origntemp2.insert(int(math.floor(float(line2[2])/10)),float(math.floor(float(line2[2])/10)))
label_orign2.append(label_origntemp2)
data_orign2.append(line2[3:])
label_np_arr2 = np.array(label_orign2)
data_np_arr2 = np.array(data_orign2)
sex_np_arr2 = np.array(sex_orign2)

data_len2 = data_np_arr2.shape[1]
data_num2 = data_np_arr2.shape[0]

label_orign = []
data_orign = []
sex_orign = []
age_orign = []
#读训练数据
with open('train.csv','rb') as precsv:
reader = csv.reader(precsv)
for line in reader:

if reader.line_num == 1:
continue
label_origntemp = [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0] #升维度
label_origntemp.insert(int(math.floor(float(line[2])/10)),float(math.floor(float(line[2])/10)))
label_orign.append(label_origntemp)
data_orign.append(line[3:])
label_np_arr = np.array(label_orign)
data_np_arr = np.array(data_orign)
#sex_np_arr = np.array(sex_orign)

data_len = data_np_arr.shape[1]
data_num = data_np_arr.shape[0]

#添加层函数
def add_layer(inputs,in_size,out_size,n_layer,activation_function=None):
layer_name='layer%s'%n_layer
with tf.name_scope('layer'):
with tf.name_scope('weights'):
Ws = tf.Variable(tf.random_normal([in_size,out_size]))
tf.histogram_summary(layer_name+'/weights',Ws)
with tf.name_scope('baises'):
bs = tf.Variable(tf.zeros([1,out_size])+0.5)
tf.histogram_summary(layer_name+'/baises',bs)
with tf.name_scope('Wx_plus_b'):
Wxpb = tf.matmul(inputs,Ws) + bs

if activation_function is None:
outputs = Wxpb
else:
outputs = activation_function(Wxpb)
tf.histogram_summary(layer_name+'/outputs',outputs)
return outputs
#比较函数
def compute_accuracy(v_xs,v_ys):
global prediction
y_pre = sess.run(prediction,feed_dict={xs:v_xs})
correct_prediction = tf.equal(tf.argmax(y_pre,1),tf.argmax(v_ys,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
result = sess.run(accuracy,feed_dict={xs:v_xs,ys:v_ys})
return result

# define placeholder for inputs to network
with tf.name_scope('inputs'):
xs = tf.placeholder(tf.float32,[None,data_len])
ys = tf.placeholder(tf.float32,[None,10])

#3个隐藏层
l1 = add_layer(xs,data_len,19,n_layer=1,activation_function=tf.nn.sigmoid)
l2 = add_layer(l1,19,19,n_layer=2,activation_function=tf.nn.sigmoid)
l3 = add_layer(l2,19,19,n_layer=3,activation_function=tf.nn.sigmoid)
# add output layer
prediction = add_layer(l3,19,10,n_layer=4,activation_function=tf.nn.softmax)

with tf.name_scope('loss'):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(ys*tf.log(prediction),reduction_indices=[1]))
tf.scalar_summary('loss',cross_entropy) #show in evernt
with tf.name_scope('train'):
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy)

init = tf.initialize_all_variables()

saver = tf.train.Saver()
sess = tf.Session()
merged = tf.merge_all_summaries()
writer = tf.train.SummaryWriter("logs/", sess.graph)
sess.run(init)

for i in range(10000):
_, cost = sess.run([train_step, cross_entropy], feed_dict={xs:data_np_arr,
ys:label_np_arr.reshape((data_num,10))})
#sess.run(train_step,feed_dict={xs:data_np_arr,ys:label_np_arr.reshape((data_num,10))})
if i%50 == 0:
print("Epoch:", '%04d' % (i), "cost=", \
"{:.9f}".format(cost),"Accuracy:",compute_accuracy(data_np_arr2,label_np_arr2.reshape((data_num2,10))))
result = sess.run(merged,feed_dict={xs:data_np_arr,
ys:label_np_arr.reshape((data_num,10))})
writer.add_summary(result,i)

print("Optimization Finished!")
训练过程

学习笔记

bandit算法原理及Python实现
http://blog.csdn.net/z1185196212/article/details/53374194
tensorflow基础笔记
http://blog.csdn.net/z1185196212/article/details/53817067
可视化
http://blog.csdn.net/z1185196212/article/details/53842633

安装运行方法和Demo

我的版本库地址:
https://coding.net/u/zhaoxinyan/p/np2016/git

运行环境

# 安装numpy
sudo apt-get install python-numpy 
# 安装opencvsudo 
apt-get install python-opencv 
#安装OCR和预处理相关依赖
sudo apt-get install tesseract-ocr
sudo pip install pytesseract
sudo apt-get install python-tk
sudo pip install pillow
# 安装Flask框架、mongo
sudo pip install Flask
sudo apt-get install mongodb # 如果找不到可以先sudo apt-get update
sudo service mongodb started
sudo pip install pymongo

运行

cd BloodTestReportOCR
python view.py # upload图像,在浏览器打开http://yourip:8080
运行view.py

上传报告
生成报告
预测
当传入了一张猫的图片时

事后复盘

陆放翁有诗云:

古人学问无遗力,少壮工夫老始成。纸上得来终觉浅,绝知此事要躬行。

此话倒是一点不假,经过这门实践课的学习我真的明显的感觉到了我的代码能力的提升。。。
首先在A1我就困难重重,虽然代码都是现成的(晕。。。),我的电脑中原先装的是python3.x,与python2.x各种不兼容。所以卸了重装才跑起来。
A2的时候我装的是openCV3,版本库上用的是CV2。。。再加上我刚开学时年少无知,居然装了Ubuntu kylin!各种环境问题,解决一个又来一个,果断重装系统后才终于没有环境问题了。果然Ubuntu14还是比较好的。
A3开始就需要学习机器学习算法了,因为我之前了解了一些机器学习的相关内容,所以这部分相对(只是相对!)轻松了一些。加上我们又站在了巨人的肩膀上,我感觉对于程序员来讲,当了解算法的具体细节遇到困难时,了解已有库的封装和使用方式就变得尤其重要了。我学习的是谷歌的Tensorflow,它在实现神经网络上还是很给力的。对于这个库来说,主要需要了解的是节点、数据流、图等相关概念。想要弄的漂亮的话还可以做一些可视化处理。TensorFlow相对来说还是比较底层的一个库,比如Keras就是一个高层神经网络库,Keras由纯Python编写而成并基于Tensorflow或Theano。
中间要pr还学了git
最后写博客还学了markdown,哈哈
谢谢一起学习的同学们,感谢孟宁老师!感谢我们一些学习的时光!

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

推荐阅读更多精彩内容

  • 大约有几十年没有骑过自行车了,前不久看到有小黄车在路边,说是扫码即骑,一小时一元。当时觉得有点意思,但也没有想...
    宋家铨阅读 120评论 0 0
  • 我在南方的海岛上看到很多椰树。有的一棵一棵,整齐排列在路边。也有不是成群的,散落在民居的院子,散落在乡下的田地。没...
    王梓枫阅读 356评论 0 1
  • 有一种姑娘,上班、上学、逛街、约会、买菜甚至倒垃圾都会化妆,化妆是一种生活状态,并不会因为做什么而改变,她们的美无...
    梁颖书阅读 432评论 0 1