微博大数据第二期:作弊头像识别

@作者: 研发中心算法平台 @迪吉老农

假冒大V的头像识别

一. 问题的定义

热门微博的评论中出现了一批假冒名人头像的用户,在头像右下角仿造了一个V字图形。如图,


热门流中的假大V

热门微博的产品希望通过图片识别这些用户。

二. 基于相似图片的方法

2.1. 算法

第一版模型,是基于相似图片搜索。步骤如下,

  • 首先选出来一些V字图片作为模板,提取图片的指纹特征
  • 对于给定的一张要判定的图片,按像素和不同放大比例来扫描,如果发现和模板中的指纹相似性较高,则认为含有V字形在图片里面。

图片指纹的计算,参考的是图片搜索引擎这篇文章。简单来说,对于一张给定的方块图,我们把它按照下图所示分成5个区块,

图片分块

每个区块计算HSV(Hue, Saturation, Value)三维颜色空间的直方图分布,例如,

将这个分布的形状作为图片的指纹特征。通过比较每个区块中的颜色分布与模板的颜色分布是否相似(计算$\chi^2$距离),来判定是否含有大V图标。

2.2. 效果

我们手工收集了15张含有V字的图片,将V字部分抠出来,计算指纹作为模板保存起来。

通过上面的算法,成功识别了大V图形的部分。如下图所示,

大V模板上的表现

算法的计算时间比较长,因为要考虑不同位置和不同缩放比例。平均完成一整张图的扫描的时间为1s。

当我们把这个算法应用到100万个测试用的头像集的时候,更严重的问题出现了:误判。很多正常的图片(比如一些纯色图、漫画)都被错误地识别成大V了。我们试图通过调整直方图的划分方式来过滤误伤,但是,新的误伤类型又会产生,此消彼长。一些误判的例子如下,

最终,从100万张测试图像中扫出300张图,竟然没有一张是大V图片,100%都是误判。我们意识到,大V图片的在总比例是相当低的,需要更为精确的算法才能避免误伤。

三. 卷积神经网络

之前的算法是,采用直方图提取特征值,用扫描和缩放来定位。我们考虑,这两步可以统一到卷积神经网络的框架中,特征可以提取得更细腻,扫描和缩放也可以通过卷积和池化的操作来实现,避免了扫描带来的大量重复计算,可以把精度和速度都提升一个量级。卷积神经网络应用于手写识别已经很成功,解决类似的问题,我们猜测只需要一个简单的网络就可以了。只是难点在于,没有训练数据。

3.1. 数据生成

构建深度网络模型需要大量的标注数据,这在我们的项目中是无法获得的,只能我们人为构造数据集,但是需要加入足够丰富的扰动项,以避免模型抓住了单一维度,过拟合。

数据的生成方式如下,

  • 首先,从之前的模板中将V字图形抠出。
  • 然后,对V字图形进行一定的随机扰动,包括
    • 一些photoshop中的手动操作(比如增加水波纹理,灰色图,特效等)
    • 机器自动生成的缩放、旋转、色阶移动等。
  • 将扰动后的大V附加到原图右下角的位置上。
  • 模仿之前的假大V模式,按一定比例地用圆圈切除四个边角,用灰白色填充空白。

生成器生成的负样本如下(只显示了右下角),

无大V的图片

正样本如下(只显示右下角),

正样本

最终,读取一个大概60000张的非大V的头像图库,把按照1:1的比例持续输出正负样本给模型进行训练。

3.2. 网络结构和训练参数

之前很多次用卷积神经网络,但是没有自己写过模型,都是直接用前人写好的网络结构跑一下。很多时候,好的结果自动就出来了,也没有深究每一步的原因。这次干脆从零开始写,实验了一下各种模型结构。

最后发现,如果随便弄上去一个模型,也许可以都达到差不多99%的准确率;但是,这样的准确率并不够,1%的误差意味着,一百万用户就误伤1万用户。我们要看哪个模型可以达到99.90%,甚至接近99.99%,才能应对这种正负样本偏差极大的情况。

3.2.1. LeNet结构

开始试验的是类似LeNet的结构,基本结构是Convolution2D + MaxPooling,然后不断循环加深。

Layer (type)                 Output Shape              Param #   
=================================================================
image_input (InputLayer)     (None, 90, 90, 3)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 90, 90, 8)         224       
_________________________________________________________________
batch_normalization_1 (Batch (None, 90, 90, 8)         32        
_________________________________________________________________
activation_1 (Activation)    (None, 90, 90, 8)         0         
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 8)         0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 45, 45, 8)         584       
_________________________________________________________________
batch_normalization_2 (Batch (None, 45, 45, 8)         32        
_________________________________________________________________
activation_2 (Activation)    (None, 45, 45, 8)         0         
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 8)         0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 22, 22, 16)        1168      
_________________________________________________________________
batch_normalization_3 (Batch (None, 22, 22, 16)        64        
_________________________________________________________________
activation_3 (Activation)    (None, 22, 22, 16)        0         
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 16)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        4640      
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 32)        128       
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 32)        0         
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 5, 5, 32)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 800)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 16)                12816     
_________________________________________________________________
batch_normalization_5 (Batch (None, 16)                64        
_________________________________________________________________
activation_5 (Activation)    (None, 16)                0         
_________________________________________________________________
output (Dense)               (None, 2)                 34        
=================================================================
Total params: 19,786
Trainable params: 19,626
Non-trainable params: 160

这个模型在测试集上的效果如下,感觉对V字的识别能力还是不错的,误伤在30%以上,略大了一些。

Precision-recall at threshold 0.500000
             precision    recall  f1-score   support

          0       0.99      0.89      0.94       373
          1       0.76      0.97      0.85       131

avg / total       0.93      0.91      0.91       504

Precision-recall at threshold 0.700000
             precision    recall  f1-score   support

          0       0.99      0.88      0.93       373
          1       0.80      0.95      0.87       131

avg / total       0.94      0.90      0.92       504

Precision-recall at threshold 0.800000
             precision    recall  f1-score   support

          0       0.99      0.86      0.92       373
          1       0.83      0.95      0.88       131

avg / total       0.95      0.88      0.91       504

Precision-recall at threshold 0.900000
             precision    recall  f1-score   support

          0       0.99      0.83      0.90       373
          1       0.87      0.94      0.90       131

avg / total       0.96      0.86      0.90       504

Precision-recall at threshold 0.950000
             precision    recall  f1-score   support

          0       0.99      0.82      0.89       373
          1       0.89      0.94      0.91       131

avg / total       0.96      0.85      0.90       504

AUC score:
0.983238851483
confusion_matrix
[332  41] (0) 0
[  4 127] (1) 1
 (0) (1)

3.2.2 全卷积的LeNet结构

LeNet结构的模型,经过Flat层以后,变成了800,全链接层带来了比较多的参数,有过拟合的风险,于是尝试用GlobalMaxPooling替代Flat层和隐层Dense(16)。

Layer (type)                 Output Shape              Param #   
=================================================================
image_input (InputLayer)     (None, 90, 90, 3)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 90, 90, 8)         224       
_________________________________________________________________
batch_normalization_1 (Batch (None, 90, 90, 8)         32        
_________________________________________________________________
activation_1 (Activation)    (None, 90, 90, 8)         0         
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 8)         0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 45, 45, 8)         584       
_________________________________________________________________
batch_normalization_2 (Batch (None, 45, 45, 8)         32        
_________________________________________________________________
activation_2 (Activation)    (None, 45, 45, 8)         0         
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 8)         0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 22, 22, 16)        1168      
_________________________________________________________________
batch_normalization_3 (Batch (None, 22, 22, 16)        64        
_________________________________________________________________
activation_3 (Activation)    (None, 22, 22, 16)        0         
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 16)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        12832     
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 32)        128       
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 32)        0         
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 5, 5, 32)          0         
_________________________________________________________________
global_max_pooling2d_1 (Glob (None, 32)                0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 32)                0         
_________________________________________________________________
output (Dense)               (None, 2)                 66        
=================================================================
Total params: 15,130
Trainable params: 15,002
Non-trainable params: 128

这个模型在测试集的效果如下,

Precision-recall at threshold 0.500000
             precision    recall  f1-score   support

          0       0.95      0.94      0.94       373
          1       0.82      0.85      0.84       131

avg / total       0.92      0.91      0.92       504

Precision-recall at threshold 0.700000
             precision    recall  f1-score   support

          0       0.96      0.92      0.94       373
          1       0.87      0.80      0.83       131

avg / total       0.93      0.89      0.91       504

Precision-recall at threshold 0.800000
             precision    recall  f1-score   support

          0       0.97      0.91      0.94       373
          1       0.88      0.78      0.83       131

avg / total       0.95      0.88      0.91       504

Precision-recall at threshold 0.900000
             precision    recall  f1-score   support

          0       0.99      0.90      0.94       373
          1       0.89      0.71      0.79       131

avg / total       0.96      0.85      0.90       504

Precision-recall at threshold 0.950000
             precision    recall  f1-score   support

          0       0.99      0.88      0.93       373
          1       0.91      0.64      0.75       131

avg / total       0.97      0.82      0.89       504

AUC score:
0.97599410597
confusion_matrix
[349  24] (0) 0
[ 19 112] (1) 1
 (0) (1)

观察分错的例子,我们发现对于红黄交替的图片会分不清,对于圆形标记内部的V字形没有分辨能力。

LeNet分错的例子

我感觉模型并没有学习到V的形状,对于含有V字的变体,没有任何识别能力。

漏掉的大V

为了解决这个问题,我曾一度尝试在网络后层加深,或者增加卷积的输出频道个数;但是除了模型过拟合变的更严重外,效果没有改进。

3.2.3. 网络结构分析

在之前的建模中,我一直有一个误区:注意到V字图形的边长是右下角$\frac{1}{3}$左右,于是希望卷积后的图片大小能越来越小,到最后一层能到$5\times5$或$3\times3$,以为这样以后每个点控制的就是$\frac{1}{5}$或者$\frac{1}{3}$的图像区域,就可以覆盖到完整的V字。实际上这样理解卷积是错误的。

实际上应该怎么计算,我们看下图,


卷积和池化覆盖

我们先看左边的卷积。如果上层每个结点覆盖了$c_n$个像素点,结点之间的步长跨度为$step_n$像素,那么一个步长为$k$的卷积生成的新结点,将覆盖像素点个数为
$$
c_{n+1} = c_n + (k-1)\cdot step_n
$$
再看右边的池化层。池化层的作用是修改了结点之间步长跨度的像素,即
$$
step_{n+1} = 2 step_n
$$
拿之前的两层网络分析一下,

Layer (type)                 Output Shape              Param #   
=================================================================
image_input (InputLayer)     (None, 90, 90, 3)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 90, 90, 8)         224       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 8)         0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 45, 45, 8)         584       
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 8)         0         
______________________________________________________________
conv2d_3 (Conv2D)            (None, 22, 22, 16)        1168      
_________________________________________________________________
activation_3 (Activation)    (None, 22, 22, 16)        0         
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 16)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 11, 11, 32)        12832     
_________________________________________________________________ 
  • 第1层卷积,核是3,步长是1个像素,每个上层结点对应1个像素,所以每个下层结点覆盖像素点个数为$1 + (3-1) \cdot 1 = 3$
  • 第1层池化,将2个像素点合成一个步长。
  • 第2层卷积,核是3,步长是2个像素,每个上层结点对应3个像素,所以下层结点覆盖$3 + (3-1)\cdot2 = 7$
  • 第2层池化,将4个像素点合成一个步长。
  • 第3层卷积,核是3,步长是4个像素,下层结点覆盖$7 + (3-1)\cdot 4 = 15$
  • 第3层池化,将8个像素点合成一个步长。
  • 第4层卷积,核是3,步长是8个像素,下层结点覆盖$15 + (3-1)\cdot 8 = 31$

我们看到,经过这些步骤以后,图像抽象到了$11\times11$个结点,每个结点覆盖31个像素点。已经达到了我们的预期的V字图片的大小,如果再做一次卷积,将结点个数缩小到$5\times5$,那么每个点将覆盖$31 + (3-1)\cdot16 = 63$了,已经够大了,并不是之前想的,只覆盖了$90 \times \frac{1}{5} = 16$的像素点。

3.2.4. VGG结构

最终选用VGG类似的结构,主要的结构模式是把两次$3\times3$的卷积叠在一起,再做池化。相当于增加了抽象的程度。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
image_input (InputLayer)     (None, 90, 90, 3)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 90, 90, 16)        448       # 1 + 2 * 1 = 3
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 90, 90, 16)        2320      # 3 + 2 * 1 = 5
_________________________________________________________________
batch_normalization_1 (Batch (None, 90, 90, 16)        64        
_________________________________________________________________
activation_1 (Activation)    (None, 90, 90, 16)        0         
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 16)        0         # step = 2
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 45, 45, 16)        2320      # 5 + 2 * 2 = 9
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 45, 45, 16)        2320      # 9 + 2 * 2 = 13
_________________________________________________________________
batch_normalization_2 (Batch (None, 45, 45, 16)        64        
_________________________________________________________________
activation_2 (Activation)    (None, 45, 45, 16)        0         
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 16)        0         # step = 4
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 22, 22, 32)        4640      # 13 + 2 * 4 = 21
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 22, 22, 32)        9248      # 21 + 2 * 4 = 29
_________________________________________________________________
batch_normalization_3 (Batch (None, 22, 22, 32)        128       
_________________________________________________________________
activation_3 (Activation)    (None, 22, 22, 32)        0         
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 32)        0         # step = 8
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 11, 11, 48)        13872     # 29 + 2 * 8 = 45
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 11, 11, 48)        20784     # 45 + 2 * 8 = 61
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 48)        192       
_________________________________________________________________
activation_4 (Activation)    (None, 11, 11, 48)        0         
_________________________________________________________________
global_max_pooling2d_1 (Glob (None, 48)                0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 48)                0         
_________________________________________________________________
output (Dense)               (None, 2)                 98        
=================================================================
Total params: 56,498
Trainable params: 56,274
Non-trainable params: 224

最后每个点覆盖的像素点个数,可以按照上表中的注释部分来计算,得到最终$11\times11$个结点,每个结点覆盖61个像素,比较好的可以覆盖各种尺寸的V字图标。

我们将之前模型预测错误的样本加入模型,不断迭代训练。这过程不仅包含增加训练集,而是在测试集和验证集中也都增加了一批模型分错的图片。目前的效果是,

classification report:
Precision-recall at threshold 0.500000
             precision    recall  f1-score   support

          0       1.00      0.99      0.99      6820
          1       0.94      0.99      0.97      1124

avg / total       0.99      0.99      0.99      7944

Precision-recall at threshold 0.700000
             precision    recall  f1-score   support

          0       1.00      0.98      0.99      6820
          1       0.96      0.99      0.97      1124

avg / total       0.99      0.98      0.99      7944

Precision-recall at threshold 0.800000
             precision    recall  f1-score   support

          0       1.00      0.98      0.99      6820
          1       0.97      0.99      0.98      1124

avg / total       0.99      0.98      0.99      7944

Precision-recall at threshold 0.900000
             precision    recall  f1-score   support

          0       1.00      0.97      0.98      6820
          1       0.97      0.98      0.98      1124

avg / total       1.00      0.97      0.98      7944

Precision-recall at threshold 0.950000
             precision    recall  f1-score   support

          0       1.00      0.96      0.98      6820
          1       0.98      0.98      0.98      1124

avg / total       1.00      0.96      0.98      7944

AUC score:
0.998549835109
confusion_matrix
[6747   73] (0) 0
[   6 1118] (1) 1
 (0) (1)

3.3. 实际效果

几轮迭代以后,0.9分数下实际分错的比例从90%降低到10%以内,召回保持在90%以上。

四. 持续训练架构

我将模型的训练抽象出来,方便模型持续接受新的输入来迭代。

  • 启动vreco_train_booter.sh,程序会每隔30秒搜索一次log/start.pid存在与否,判断是否启动训练。
  • 将新增的图片上传到upload文件夹中对应的类目中,比如将模型分错的图片传到upload/train/0/目录下,将含有大V标识的图片传到upload/train/1/目录下。
  • log目录下新建start.pid来通知vreco_train_booter.sh启动模型训练。
  • 启动模型后,程序将upload目录下的图片合并到stage目录下。
  • 训练时间大概是不到一个小时,平台会生成一个版本编号,生成这个版本对应的结果日志,保存到backup/result/目录下。模型文件保存到backup/model/。最后会将这次训练用到的数据stage打包放在backup/data/目录中。所有这些都会以版本号来区分,例如,
├── data
│   ├── stage_20170516-154945.tar.gz
│   ├── stage_20170516-163435.tar.gz
│   └── stage_20170516-174022.tar.gz
├── model
│   ├── cnn_20170516-154945.pkl
│   ├── cnn_20170516-154945.pkl.hdf5
│   ├── cnn_20170516-163435.pkl
│   ├── cnn_20170516-163435.pkl.hdf5
│   ├── cnn_20170516-174022.pkl
│   ├── cnn_20170516-174022.pkl.hdf5
│   └── nn_checkpoint.hdf5
└── result
    ├── result_20170516-154945
    ├── result_20170516-163435
    └── result_20170516-174022
  • 训练结束后会通过邮件,把结果result发送到指定的邮箱中。

我们可以通过报告来查看是否新的模型有合理的表现,是否需要上线等。如果需要恢复上一次版本的数据重新训练,可以从backup/data/中找到对应的stage解压回去。

注意,GPU内存需要保持将近3G空余用来训练,否则会报错,发送带有错误标题的空邮件报告。

版权声明

以上文章为本人@迪吉老农原创,首发于简书,文责自负。文中如有引用他人内容的部分(包括文字或图片),均已明文指出,或做出明确的引用标记。如需转载,请联系作者,并取得作者的明示同意。感谢。

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

推荐阅读更多精彩内容