推薦系統(tǒng)之DeepFM模型原理以及代碼實(shí)踐

簡介

本文要介紹的是由哈爾濱工業(yè)大學(xué)聯(lián)合華為發(fā)表論文《DeepFM: A Factorization-Machine based Neural Network for CTR Prediction》中提出的DeepFM模型。其實(shí)根據(jù)名字可以看出來,此模型包含Deep和FM兩個(gè)部分。其中Deep部分就是普通的深度神經(jīng)網(wǎng)絡(luò)塘安,F(xiàn)M是因子分解機(jī)(Factorization Machine)挥等,用于來做特征交叉削咆。DeepFM實(shí)際上是將FM模型與Wide&Deep模型進(jìn)行了整合。DeepFM對(duì)Wide&Deep模型的改進(jìn)之處在于迅矛,它使用了FM來替換了原來的Wide部分棒坏,加強(qiáng)了淺層網(wǎng)絡(luò)部分特征組合的能力燕差。為了更好地理解DeepFM模型遭笋,讀者最好先了解一下FM模型和Wide&Deep模型坝冕。可以參考這兩篇博客:

介紹

在推薦系統(tǒng)中瓦呼,學(xué)習(xí)隱藏在用戶行為數(shù)據(jù)背后的復(fù)雜特征交互對(duì)最大化CTR任務(wù)起著重要的作用喂窟,而提高CTR的預(yù)估準(zhǔn)確率能夠直接為企業(yè)帶來豐厚的利益。很多在線廣告系統(tǒng)是根據(jù)CTR \times bid來對(duì)候選廣告進(jìn)行排名的央串,其中bid代表用戶每次點(diǎn)擊廣告時(shí)系統(tǒng)能獲得的收益磨澡。
學(xué)習(xí)用戶點(diǎn)擊行為背后的隱藏特征交互對(duì)CTR預(yù)估任務(wù)來說十分重要。作者通過對(duì)主流的app應(yīng)用市場調(diào)查研究發(fā)現(xiàn)质和,用戶常常在用餐時(shí)間下載外賣類app稳摄,這就是一種對(duì)”app類別“和“時(shí)間”這兩種特征的二階交互信息,這類的二階交互信息可以用于CTR預(yù)估任務(wù)饲宿。再比如厦酬,男性青少年偏愛射擊類和角色扮演游戲,這些信息是3階的瘫想。即包含了”性別“仗阅,”年齡“和”app類別“,這也有助于CTR預(yù)估任務(wù)国夜。這些用戶行為背后的特征交互是非常復(fù)雜的减噪,這些特征包括了低階和高階的交互信息。2016年谷歌提出的Wide & Deep模型對(duì)特征進(jìn)行了低階和高階交互车吹,整體上提高了模型性能筹裕。然而,Wide&Deep模型中的”Wide“部分仍然需要依賴手工特征提取窄驹。

DeepFM模型

為了能夠同時(shí)進(jìn)行低階和高階的特征融合朝卒,并且以端到端的方式訓(xùn)練,論文作者提出了DeepFM模型馒吴,模型結(jié)構(gòu)圖如下:
DeepFM模型

DeepFM模型實(shí)際上延續(xù)了Wide&Deep雙模型組合的結(jié)構(gòu)扎运。它使用FM去替換了Wide&Deep中的Wide部分瑟曲,加強(qiáng)了淺層網(wǎng)絡(luò)部分特征組合的能力。上圖中豪治,左側(cè)的FM部分與右側(cè)的Deep神經(jīng)網(wǎng)絡(luò)部分共享同樣的Embedding層洞拨。左側(cè)的FM部分對(duì)不同的特征域的Embedding進(jìn)行了兩兩交叉,也就是將Embedding向量當(dāng)做原FM中的特征隱向量负拟。最后將FM的輸出與Deep部分的輸出一同輸入到最后的輸出層烦衣,參與最后的目標(biāo)擬合。

與Wide&Deep模型相比掩浙,DeepFM模型的改進(jìn)主要是針對(duì)Wide&Deep模型中Wide部分不具備特征自動(dòng)組合能力的缺陷而進(jìn)行的花吟。這里的改動(dòng)動(dòng)機(jī)與Deep&Cross模型完全一致,唯一的不同在于Deep&Cross模型利用多層Cross網(wǎng)絡(luò)進(jìn)行特征組合厨姚,而DeepFM使用了FM進(jìn)行特征組合衅澈。關(guān)于Deep&Cross模型,可以參考推薦系統(tǒng)之Deep&Cross模型原理以及代碼實(shí)踐谬墙。

假設(shè)訓(xùn)練數(shù)據(jù)集中包含n個(gè)實(shí)例(\chi, y)今布,其中\chi是一個(gè)包含m個(gè)特征域的數(shù)據(jù)記錄,通常包含用戶和物品數(shù)據(jù)拭抬。y \in \{0,1\}是對(duì)應(yīng)的標(biāo)簽數(shù)據(jù)部默,代表用戶的點(diǎn)擊行為(1代表用戶點(diǎn)擊,0代表沒點(diǎn)擊)造虎。\chi中包含了許多類別域傅蹂,比如性別,地點(diǎn)以及連續(xù)變量算凿,比如年齡等份蝴。每一個(gè)類別特征域都通過one-hot進(jìn)行編碼,每個(gè)連續(xù)特征域就代表它本身澎媒,或者可以通過離散化之后通過one-hot進(jìn)行表征搞乏。那么, 每個(gè)實(shí)例可以被轉(zhuǎn)換成(x,y)戒努,其中x=\left[x_{\text {field}_{1}}, x_{\text {field}_{2}}, \ldots, x_{\text {filed}_{j}}, \ldots, x_{\text {field}_{m}}\right]是一個(gè)d維向量请敦,其中x_{\text {filed}_{j}}\chi中第j個(gè)特征域的向量化表示。一般來說储玫,x是一個(gè)高維的及其稀疏的向量侍筛。
DeepFM中的FM部分可以學(xué)習(xí)一階和二階特征交互。對(duì)于特征i撒穷,標(biāo)量w_i可以用來衡量它的一階重要程度匣椰,隱向量V_i可以用來衡量它與其它特征之間的交互的影響。V_i被傳入FM模型中去捕捉2階特征交互端礼,同時(shí)被傳入到Deep部分中去捕獲更高階的特征交互禽笑。所有的參數(shù)包括w_i入录,V_i都是網(wǎng)絡(luò)的參數(shù),都通過以下聯(lián)合預(yù)測模型來共同訓(xùn)練:


下面依次從FM和Deep兩個(gè)部分來詳細(xì)描述DeepFM模型佳镜。

FM部分

FM部分的結(jié)構(gòu)圖如下:

FM部分結(jié)構(gòu)圖

FM模型主要的作用是能夠在數(shù)據(jù)極其稀疏的情況下高效地學(xué)習(xí)到組合特征僚稿,包括2階以及更高階數(shù)。而在以往的方式中蟀伸,特征i和特征j之間的交互項(xiàng)參數(shù)只有當(dāng)特征i和特征j同時(shí)出現(xiàn)在一條輸入數(shù)據(jù)中才可能得到學(xué)習(xí)蚀同。FM使用了加法單元和一些列的內(nèi)積單元來實(shí)現(xiàn),公式如下:
關(guān)于FM部分不再贅述啊掏,有興趣的同學(xué)請(qǐng)參考推薦系統(tǒng)之FM(因子分解機(jī))模型原理以及代碼實(shí)踐蠢络。

Deep部分

Deep部分就更簡單了,就是一個(gè)前向深度網(wǎng)絡(luò)迟蜜,它用來學(xué)習(xí)更高階的特征交互刹孔。Deep部分如下:
Deep部分結(jié)構(gòu)圖

這里的DNN與圖像或者音頻領(lǐng)域使用的DNN不同,這些領(lǐng)域的輸出數(shù)據(jù)通常都是連續(xù)且稠密的小泉。而CTR預(yù)估任務(wù)中的數(shù)據(jù)通常是極度稀疏芦疏,超高維的冕杠,并且是類別數(shù)據(jù)和連續(xù)數(shù)據(jù)混合的微姊,因此需要從網(wǎng)絡(luò)結(jié)構(gòu)上進(jìn)行改變。因此在輸入數(shù)據(jù)和Hidden Layer中間有一個(gè)Dense EMbeddings層分预,它的主要作用就是將稀疏高維的數(shù)據(jù)壓縮成稠密的實(shí)數(shù)向量兢交。

輸入層到Embedding層的子網(wǎng)絡(luò)結(jié)構(gòu)如下圖所示:
Embedding層結(jié)構(gòu)
這里需要指出這個(gè)網(wǎng)絡(luò)結(jié)構(gòu)的兩個(gè)有趣的特性:
  1. 盡管輸入向量長度可能不同,但是他們的Embedding大小都是相同的(比如為k)笼痹。
  2. FM中的隱向量V現(xiàn)在直接變成了DeepFM模型中的網(wǎng)絡(luò)權(quán)重參數(shù)配喳,它們被用來將輸入數(shù)據(jù)壓縮成embedding向量。

FFN模型使用的是一個(gè)預(yù)訓(xùn)練好的FM用來做網(wǎng)絡(luò)參數(shù)初始化凳干,而DeepFM是相當(dāng)于直接把FM集成到了模型內(nèi)部晴裹,跟著模型一起訓(xùn)練優(yōu)化。這樣就消除了FM的預(yù)訓(xùn)練過程救赐,整個(gè)網(wǎng)絡(luò)就可以使用端到端的方式來進(jìn)行訓(xùn)練了涧团。我們可以將Embedding層的輸出表示為:

其中e_i是第i個(gè)特征域的Embedding表示,m是特征域的個(gè)數(shù)经磅。a^{(0)}被輸入到DNN中泌绣,計(jì)算方式為:
其中l是隱層的深度,\sigma是激活函數(shù)预厌。經(jīng)過DNN后阿迈,一個(gè)稠密的實(shí)值向量就產(chǎn)生了,最終被送到激活函數(shù)中進(jìn)行CTR預(yù)估:
其中|H|是隱層的數(shù)量轧叽。
特別值得注意的是苗沧,F(xiàn)M部分和Deep部分是共享同樣的特征embedding表示的刊棕,這樣做帶來了兩個(gè)好處:

  • 它可以同時(shí)從原始特征中學(xué)習(xí)低階和高階的特征組合
  • 無需對(duì)輸入進(jìn)行額外的特征工程,而在Wide&Deep中是需要的

DeepFM與其他神經(jīng)網(wǎng)絡(luò)對(duì)比

作者將DeepFM與FNN待逞、PNN鞠绰、Wide&Deep等進(jìn)行了對(duì)比,分別分析了各自的優(yōu)缺點(diǎn)飒焦,總結(jié)出了下表:
模型對(duì)比

可以看到DeepFM模型是唯一一個(gè)不需要預(yù)訓(xùn)練和特征工程蜈膨,且可以同時(shí)捕獲低階和高階特征交互的模型。

代碼實(shí)踐

模型部分:

import torch
import numpy as np
import torch.nn as nn
import torch.utils.data as Data
from torch.utils.data import DataLoader
import torch.optim as optim
import torch.nn.functional as F
from sklearn.metrics import log_loss, roc_auc_score
from collections import OrderedDict, namedtuple, defaultdict
from BaseModel.basemodel import BaseModel

class DeepFM(BaseModel):
    def __init__(self, config, feat_sizes, sparse_feature_columns, dense_feature_columns):
        super(DeepFM, self).__init__(config)
        self.feat_sizes = feat_sizes
        self.device = config['device']
        self.sparse_feature_columns = sparse_feature_columns
        self.dense_feature_columns = dense_feature_columns
        self.embedding_size = config['ebedding_size']
        self.l2_reg_linear = config['l2_reg_linear']

        # self.feature_index 建立feature到列名到輸入數(shù)據(jù)X的相對(duì)位置的映射
        self.feature_index = self.build_input_features(self.feat_sizes)

        self.bias = nn.Parameter(torch.zeros((1,)))
        # self.weight
        self.weight = nn.Parameter(torch.Tensor(len(self.dense_feature_columns), 1)).to(self.device)
        torch.nn.init.normal_(self.weight, mean=0, std=0.0001)

        # FM的Linear部分牺荠,即對(duì)每一個(gè)特征乘上一個(gè)權(quán)重系數(shù)翁巍,由于有稠密和稀疏特征,因此這里分為了兩部分休雌。
        self.embedding_dict1 = self.create_embedding_matrix(self.sparse_feature_columns, feat_sizes, 1, device=self.device)
        self.embedding_dict2 = self.create_embedding_matrix(self.sparse_feature_columns, feat_sizes, self.embedding_size, device=self.device)

        # Deep
        self.dropout = nn.Dropout(self._config['dnn_dropout'])
        self.dnn_input_size = self.embedding_size * len(self.sparse_feature_columns) + len(self.dense_feature_columns)
        hidden_units = [self.dnn_input_size] + self._config['dnn_hidden_units']
        self.linears = nn.ModuleList(
            [nn.Linear(hidden_units[i], hidden_units[i + 1]) for i in range(len(hidden_units) - 1)])
        self.relus = nn.ModuleList(
            [nn.ReLU() for i in range(len(hidden_units) - 1)])
        for name, tensor in self.linears.named_parameters():
            if 'weight' in name:
                nn.init.normal_(tensor, mean=0, std=config['init_std'])
        self.dnn_linear = nn.Linear(
            self._config['dnn_hidden_units'][-1], 1, bias=False).to(self.device)
        self.to(self.device)

    def forward(self, X):
        '''
            FM liner
        '''
        sparse_embedding_list1 = [self.embedding_dict1[feat](
            X[:, self.feature_index[feat][0]:self.feature_index[feat][1]].long())
            for feat in self.sparse_feature_columns]

        dense_value_list2 = [X[:, self.feature_index[feat][0]:self.feature_index[feat][1]]
                             for feat in self.dense_feature_columns]
        linear_sparse_logit = torch.sum(torch.cat(sparse_embedding_list1, dim=-1), dim=-1, keepdim=False)
        linear_dense_logit = torch.cat(dense_value_list2, dim=-1).matmul(self.weight)
        logit = linear_sparse_logit + linear_dense_logit

        sparse_embedding_list = [self.embedding_dict2[feat](
            X[:, self.feature_index[feat][0]:self.feature_index[feat][1]].long())
            for feat in self.sparse_feature_columns]
        '''
            FM second
        '''
        fm_input = torch.cat(sparse_embedding_list, dim=1)  # shape: (batch_size,field_size,embedding_size)
        square_of_sum = torch.pow(torch.sum(fm_input, dim=1, keepdim=True), 2)  # shape: (batch_size,1,embedding_size)
        sum_of_square = torch.sum(torch.pow(fm_input, 2), dim=1, keepdim=True)  # shape: (batch_size,1,embedding_size)
        cross_term = square_of_sum - sum_of_square
        cross_term = 0.5 * torch.sum(cross_term, dim=2, keepdim=False)  # shape: (batch_size,1)
        logit += cross_term

        '''
            DNN
        '''
        #  sparse_embedding_list灶壶、 dense_value_list2
        dnn_sparse_input = torch.cat(sparse_embedding_list, dim=1)
        batch_size = dnn_sparse_input.shape[0]
        # print(dnn_sparse_input.shape)
        dnn_sparse_input=dnn_sparse_input.reshape(batch_size,-1)
        # dnn_sparse_input shape: [ batch_size, len(sparse_feat)*embedding_size ]
        dnn_dense_input = torch.cat(dense_value_list2, dim=-1)
        # print(dnn_sparse_input.shape)
        # dnn_dense_input shape: [ batch_size, len(dense_feat) ]
        dnn_total_input = torch.cat([dnn_sparse_input, dnn_dense_input], dim=-1)
        deep_input = dnn_total_input

        for i in range(len(self.linears)):
            fc = self.linears[i](deep_input)
            fc = self.relus[i](fc)
            fc = self.dropout(fc)
            deep_input = fc
        dnn_output = self.dnn_linear(deep_input)

        logit += dnn_output
        '''
            output: sigmoid(DNN + FM)
        '''
        y_pred = torch.sigmoid(logit+self.bias)
        return y_pred

    def fit(self, train_input, y_label, val_input, y_val, batch_size=5000, epochs=15, verbose=5):
        x = [train_input[feature] for feature in self.feature_index]

        for i in range(len(x)):
            if len(x[i].shape) == 1:
                x[i] = np.expand_dims(x[i], axis=1)  # 擴(kuò)展成2維,以便后續(xù)cat

        # 構(gòu)造torch 數(shù)據(jù)加載器
        train_tensor_data = Data.TensorDataset(torch.from_numpy(np.concatenate(x, axis=-1)), torch.from_numpy(y_label))
        train_loader = DataLoader(dataset=train_tensor_data,shuffle=True ,batch_size=batch_size)

        print(self.device, end="\n")
        model = self.train()
        loss_func = F.binary_cross_entropy
        # loss_func = F.binary_cross_entropy_with_logits
        optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay = 0.0)
        # optimizer = optim.Adagrad(model.parameters(),lr=0.01)
        # 顯示 一次epoch需要幾個(gè)step
        sample_num = len(train_tensor_data)
        steps_per_epoch = (sample_num - 1) // batch_size + 1

        print("Train on {0} samples,  {1} steps per epoch".format(
            len(train_tensor_data), steps_per_epoch))

        for epoch in range(epochs):
            loss_epoch = 0
            total_loss_epoch = 0.0
            train_result = {}
            pred_ans = []
            true_ans = []
            with torch.autograd.set_detect_anomaly(True):
                for index, (x_train, y_train) in enumerate(train_loader):
                    x = x_train.to(self.device).float()
                    y = y_train.to(self.device).float()

                    y_pred = model(x).squeeze()

                    optimizer.zero_grad()
                    loss = loss_func(y_pred, y.squeeze(),reduction='mean')
                    #L2 norm
                    loss = loss + self.l2_reg_linear * self.get_L2_Norm()
                    loss.backward(retain_graph=True)
                    optimizer.step()
                    total_loss_epoch = total_loss_epoch + loss.item()
                    y_pred = y_pred.cpu().data.numpy()  # .squeeze()
                    pred_ans.append(y_pred)
                    true_ans.append(y.squeeze().cpu().data.numpy())

            if (epoch % verbose == 0):
                print('epoch %d train loss is %.4f train AUC is %.4f' %
                      (epoch,total_loss_epoch / steps_per_epoch,roc_auc_score(np.concatenate(true_ans), np.concatenate(pred_ans))))
                self.val_auc_logloss(val_input, y_val, batch_size=50000)
                print(" ")

    def predict(self, test_input, batch_size=256, use_double=False):
        """
        :param x: The input data, as a Numpy array (or list of Numpy arrays if the model has multiple inputs).
        :param batch_size: Integer. If unspecified, it will default to 256.
        :return: Numpy array(s) of predictions.
        """
        model = self.eval()
        x = [test_input[feature] for feature in self.feature_index]

        for i in range(len(x)):
            if len(x[i].shape) == 1:
                x[i] = np.expand_dims(x[i], axis=1)  # 擴(kuò)展成2維杈曲,以便后續(xù)cat

        tensor_data = Data.TensorDataset(torch.from_numpy(np.concatenate(x, axis=-1)))
        test_loader = DataLoader(dataset=tensor_data, shuffle=False, batch_size=batch_size)

        pred_ans = []
        with torch.no_grad():
            for index, x_test in enumerate(test_loader):
                x = x_test[0].to(self.device).float()
                # y = y_test.to(self.device).float()
                y_pred = model(x).cpu().data.numpy()  # .squeeze()
                pred_ans.append(y_pred)

        if use_double:
            return np.concatenate(pred_ans).astype("float64")
        else:
            return np.concatenate(pred_ans)

    def val_auc_logloss(self, val_input, y_val, batch_size=256, use_double=False):
        pred_ans = self.predict(val_input, batch_size)
        print("test LogLoss is %.4f test AUC is %.4f"%(log_loss(y_val, pred_ans), roc_auc_score(y_val, pred_ans)) )

    def get_L2_Norm(self ):
        loss = torch.zeros((1,), device=self.device)
        loss = loss + torch.norm(self.weight)
        for t in self.embedding_dict1.parameters():
            loss = loss+ torch.norm(t)
        for t in self.embedding_dict2.parameters():
            loss = loss+ torch.norm(t)
        return  loss

    def build_input_features(self, feat_sizes):
        # Return OrderedDict: {feature_name:(start, start+dimension)}
        features = OrderedDict()
        start = 0
        for feat in feat_sizes:
            feat_name = feat
            if feat_name in features:
                continue
            features[feat_name] = (start, start + 1)
            start += 1
        return  features

    def create_embedding_matrix(self ,sparse_feature_columns, feat_sizes, embedding_size,init_std=0.0001, device='cpu'):
        embedding_dict = nn.ModuleDict(
            {feat: nn.Embedding(feat_sizes[feat], embedding_size, sparse=False)
             for feat in sparse_feature_columns}
        )
        for tensor in embedding_dict.values():
            nn.init.normal_(tensor.weight, mean=0, std=init_std)
        return embedding_dict.to(device)

使用了一個(gè)很小的criteo數(shù)據(jù)集進(jìn)行訓(xùn)練和測試驰凛,模型很快過擬合了,不過因?yàn)橹皇莇emo担扑,也沒有繼續(xù)調(diào)整恰响。
訓(xùn)練和測試結(jié)果如下:

完整代碼見:https://github.com/HeartbreakSurvivor/RsAlgorithms/tree/main/DeepFM

測試結(jié)果

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末涌献,一起剝皮案震驚了整個(gè)濱河市胚宦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌燕垃,老刑警劉巖枢劝,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卜壕,居然都是意外死亡您旁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門轴捎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹤盒,“玉大人,你說我怎么就攤上這事轮蜕∽虻浚” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵跃洛,是天一觀的道長率触。 經(jīng)常有香客問我,道長汇竭,這世上最難降的妖魔是什么葱蝗? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任穴张,我火速辦了婚禮,結(jié)果婚禮上两曼,老公的妹妹穿的比我還像新娘皂甘。我一直安慰自己,他們只是感情好悼凑,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布偿枕。 她就那樣靜靜地躺著,像睡著了一般户辫。 火紅的嫁衣襯著肌膚如雪渐夸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天渔欢,我揣著相機(jī)與錄音墓塌,去河邊找鬼。 笑死奥额,一個(gè)胖子當(dāng)著我的面吹牛苫幢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播垫挨,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼韩肝,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了棒拂?” 一聲冷哼從身側(cè)響起伞梯,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帚屉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漾峡,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攻旦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了生逸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牢屋。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖槽袄,靈堂內(nèi)的尸體忽然破棺而出烙无,到底是詐尸還是另有隱情,我是刑警寧澤遍尺,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布截酷,位于F島的核電站,受9級(jí)特大地震影響乾戏,放射性物質(zhì)發(fā)生泄漏迂苛。R本人自食惡果不足惜三热,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望三幻。 院中可真熱鬧就漾,春花似錦、人聲如沸念搬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朗徊。三九已至夷野,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荣倾,已是汗流浹背悯搔。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舌仍,地道東北人妒貌。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像铸豁,于是被迫代替她去往敵國和親灌曙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容