torchtext学习03-联合学习对齐和翻译的神经机器翻译

Neural Machine Translation by Jointly Learning to Align and Translate

1 项目介绍

本项目采用Pytorch和Torchtext来构建seq2seq模型,以实现论文 Neural Machine Translation by Jointly Learning to Align and Translate的模型。

2 Introduction

和之前的项目一样,我们先给出一个通用的seq2seq模型。

image-20200701220104637.png

在上一个模型中,是通过在每个时间步中直接将上下文向量z传递给解码器和向全连接层f传递上下文向量、嵌入后的输入词d(y_t)和隐藏状态s_t来减少信息压缩的。

image-20200701223900656.png

虽然我们减少了一些信息压缩,但是我们的上下文向量仍然需要包含有关源句子的所有信息。在此项目中将通过使用attention来允许解码器在每个解码步骤中使用隐藏状态查看整个源句子。

attention通过以下步骤来进行:

  • 计算注意力向量a,其长度和源句子长度相同,每个元素取值为0或1,且整个向量之和为1。

  • 计算句子的隐藏状态H

  • 计算以上的加权和w
    w = \sum_{i}a_ih_i

在解码阶段,会在每个时间步计算一个新的加权源向量,并将其用作解码器RNN以及全连接层的输入来进行预测。

3 准备数据

导入模块

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

from torchtext.datasets import Multi30k
from torchtext.data import Field, BucketIterator

import spacy
import numpy as np

import random
import math
import time

设置随机种子

SEED = 1234

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

加载spacy的德语和英语模型

spacy_de = spacy.load("en_core_web_sm")
spacy_en = spacy.load("de_core_news_sm")

建立分词函数

def tokenize_de(text):
    """
    Tokenizes German text from a string into a list of strings
    """
    return [tok.text for tok in spacy_de.tokenizer(text)]


def tokenize_en(text):
    """
    Tokenizes English text from a string into a list of strings
    """
    return [tok.text for tok in spacy_en.tokenizer(text)]

建立Field

SRC = Field(tokenize=tokenize_de,
            init_token='<sos>',
            eos_token='<eos>',
            lower=True)

TRG = Field(tokenize=tokenize_en,
            init_token='<sos>',
            eos_token='<eos>',
            lower=True
            )

加载数据

train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'), 
                                                    fields = (SRC, TRG))

建立词汇表

SRC.build_vocab(train_data,min_freq=2)
TRG.build_vocab(train_data,min_freq=2)

定义设备

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

建立迭代器

train_iterator,valid_iterator,test_iterator = BucketIterator.splits(
    (train_data,valid_data,test_data),
    batch_size=BATCH_SIZE,
    device=device
)

4 建立模型

4.1 Encoder

首先建立编码器。
之前的编码器我们采用的是单层GRU,这次我们采用双向RNN。双向RNN中,每层将有两个RNN。前向RNN从左到右遍历嵌入的句子(绿色),后向RNN从右到左遍历嵌入句子(深青色)。代码中只需将bidirectional改为True即可。

image-20200701234619942-1593718396065.png

\begin{array}{l} h_{t}^{\rightarrow}=\text { EncoderGRU } \rightarrow\left(e\left(x_{t}^{\rightarrow}\right), h_{t-1}^{\rightarrow}\right) \\ h_{t}^{\leftarrow}=\text { EncoderGRU }\leftarrow\left(e\left(x_{t}^{\leftarrow}\right), h_{t-1}^{-}\right) \end{array}
其中x_0^{\rightarrow}=<sos>,x_1^{\rightarrow}=guten,x_0^{\leftarrow}=<eos>,x_1^{\leftarrow}=morgen.
和之前一样,我们仅将输入嵌入输入到RNN,同时初始化前向和后向的初始隐藏状态为0的张量。同样,我们会得到两个上下文向量,一个是前向RNN经过最后一个词之后的,z^{\rightarrow}=h_T^{\rightarrow},另一个是后向RNN经过第一个词汇后的,z^{\leftarrow}=h_T^{\leftarrow}
RNN返回的是outputs和hidden。

  • ouputs的大小是[src len,batch size,hid dim*num directions]。这里的hid dim是前向rnn的隐藏状态,hid dim*num directions可以看作是前向和后向RNN隐藏状态的叠加。比如,h_1=[h_1^{\rightarrow};h_T^{\leftarrow}],h_2=[h_2^{\rightarrow};h_{T-1}^{\leftarrow}],所有的编码器隐藏状态白表示为\mathbf{H}=\left\{h_{1}, h_{2}, \ldots, h_{T}\right\}.
  • hidden的大小是[n_layers*num directions,bathc size,hid dim],其中[-2,:,:]在最后的时间步(看到最后一个单词)后给出顶层正向RNN隐藏状态。[-1,:,:]在最后的时间步(看到第一个单词之后)之后给出顶层反向RNN隐藏状态。

由于解码器不是双向的,因此只需要一个上下文向量z即可用作其初始隐藏状态s_0。但是目前我们有两个,前向和后向z^\rightarrow=h_T^\rightarrow and z^\leftarrow=h_T^\leftarrow。我们通过将两个上下文向量连接在一起,将他们通过全连接层g,然后使用tanh激活函数来解决此问题。
z=\tanh(g(h_T^\rightarrow, h_T^\leftarrow)) = \tanh(g(z^\rightarrow, z^\leftarrow)) = s_0

这里与论文有些区别,论文中仅通过全连接层提供第一个后向RNN隐藏状态来获取上下文向量和解码器初始隐藏状态。

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
        super().__init__()
        
        self.embedding = nn.Embedding(input_dim, emb_dim)
        
        self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)
        
        self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src):
        
        #src = [src len, batch size]
        
        embedded = self.dropout(self.embedding(src))
        
        #embedded = [src len, batch size, emb dim]
        
        outputs, hidden = self.rnn(embedded)
                
        #outputs = [src len, batch size, hid dim * num directions]
        #hidden = [n layers * num directions, batch size, hid dim]
        
        #hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
        #outputs are always from the last layer
        
        #hidden [-2, :, : ] is the last of the forwards RNN 
        #hidden [-1, :, : ] is the last of the backwards RNN
        
        #initial decoder hidden is final hidden state of the forwards and backwards 
        #  encoder RNNs fed through a linear layer
        hidden = torch.tanh(self.fc(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1)))
        
        #outputs = [src len, batch size, enc hid dim * 2]
        #hidden = [batch size, dec hid dim]
        
        return outputs, hidden

4.2 Attention

接下来就是大头了,注意力机制。
注意力层会接受解码器先前的隐藏状态s_{t-1},以及编码器的所有堆叠的前后隐藏状态H。这一层将输出一个注意力向量a_h,长度是源句子的长度,每个元素在0-1之间,总和为1。
直观地说,这一层采用我们目前为止已经解码的内容s_{t-1}和所有已经编码的内容H来产生向量a_t,这个向量表示为了准确预测下一个要解码的字\hat{y}_{t+1}我们应该特别注意源句子的哪些单词。
首先,我们要计算先前解码器隐藏状态和编码器隐藏状态的能量。因为我们编码器隐藏状态是一个T大小的序列,而先前的解码器隐藏状态是单个tensor,所以我们要做的第一件事是重复前一个解码器隐藏状态T倍。然后我们将他们串联在一起并通过全连接层和tanh激活函数来计算他们之间的能量。
E_t=tanh(attn(s_{t-1},H))
可以将其视为计算每个编码器隐藏状态和先前解码器隐藏状态“匹配”的程度。
每个batch都有一个[dec_hid_dim,src_len]大小的张量,我们希望其大小是[src_len],因为注意力应该放在源语句的整个长度上,实现就是将energy乘以[1,dec_hid_dim]的张量v
\hat{a_t}=vE_t

class Attention(nn.Module):
    def __init__(self, enc_hid_dim, dec_hid_dim):
        super().__init__()
        
        self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim)
        self.v = nn.Linear(dec_hid_dim, 1, bias = False)
        
    def forward(self, hidden, encoder_outputs):
        
        #hidden = [batch size, dec hid dim]
        #encoder_outputs = [src len, batch size, enc hid dim * 2]
        
        batch_size = encoder_outputs.shape[1]
        src_len = encoder_outputs.shape[0]
        
        #repeat decoder hidden state src_len times
        hidden = hidden.unsqueeze(1).repeat(1, src_len, 1)
        
        encoder_outputs = encoder_outputs.permute(1, 0, 2)
        
        #hidden = [batch size, src len, dec hid dim]
        #encoder_outputs = [batch size, src len, enc hid dim * 2]
        
        energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim = 2))) 
        
        #energy = [batch size, src len, dec hid dim]

        attention = self.v(energy).squeeze(2)
        
        #attention= [batch size, src len]
        
        return F.softmax(attention, dim=1)

4.3 Decoder

解码器包含注意力层(attention),注意力层输入的是解码层先前隐藏状态s_{t-1}和所有编码器隐藏状态H,并返回注意力向量a_t
然后我们将使用这个注意力向量来创建权重源向量w_t,用weighted表示,它是使用a_t作为权重的编码器隐藏状态H的加权和。
w_t=a_tH
接下来,将输入的嵌入d(y_t),加权源向量w_t和先前的解码器隐藏状态s_{t-1}一起传递到解码器中,,其中d(y_t)w_t是串联在一起的。
s_t=DecoderGRU(d(y_t),w_t,s_{t-1})
然后,我们通过全连接层d(y_t)w_ts_t,来预测目标句子中的下一个单词,\hat{y}_{t+1}
\hat{y}_{t+1} = f(d(y_t), w_t, s_t)

seq2seq10-1593691173229-1593718396065.png

绿色部分是输出H的的双向RNN编码器,红色是上下文向量z = h_T = \tanh(g(h^\rightarrow_T,h^\leftarrow_T)) = \tanh(g(z^\rightarrow, z^\leftarrow)) = s_0蓝色是输出s_t的RNN解码器。紫色是线性层f输出\hat{y}_{t+1} ,橙色是a_tH的加权综合计算并输出w_t

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
        super().__init__()

        self.output_dim = output_dim
        self.attention = attention
        
        self.embedding = nn.Embedding(output_dim, emb_dim)
        
        self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)
        
        self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, input, hidden, encoder_outputs):
             
        #input = [batch size]
        #hidden = [batch size, dec hid dim]
        #encoder_outputs = [src len, batch size, enc hid dim * 2]
        
        input = input.unsqueeze(0)
        
        #input = [1, batch size]
        
        embedded = self.dropout(self.embedding(input))
        
        #embedded = [1, batch size, emb dim]
        
        a = self.attention(hidden, encoder_outputs)
                
        #a = [batch size, src len]
        
        a = a.unsqueeze(1)
        
        #a = [batch size, 1, src len]
        
        encoder_outputs = encoder_outputs.permute(1, 0, 2)
        
        #encoder_outputs = [batch size, src len, enc hid dim * 2]
        
        weighted = torch.bmm(a, encoder_outputs)
        
        #weighted = [batch size, 1, enc hid dim * 2]
        
        weighted = weighted.permute(1, 0, 2)
        
        #weighted = [1, batch size, enc hid dim * 2]
        
        rnn_input = torch.cat((embedded, weighted), dim = 2)
        
        #rnn_input = [1, batch size, (enc hid dim * 2) + emb dim]
            
        output, hidden = self.rnn(rnn_input, hidden.unsqueeze(0))
        
        #output = [seq len, batch size, dec hid dim * n directions]
        #hidden = [n layers * n directions, batch size, dec hid dim]
        
        #seq len, n layers and n directions will always be 1 in this decoder, therefore:
        #output = [1, batch size, dec hid dim]
        #hidden = [1, batch size, dec hid dim]
        #this also means that output == hidden
        assert (output == hidden).all()
        
        embedded = embedded.squeeze(0)
        output = output.squeeze(0)
        weighted = weighted.squeeze(0)
        
        prediction = self.fc_out(torch.cat((output, weighted, embedded), dim = 1))
        
        #prediction = [batch size, output dim]
        
        return prediction, hidden.squeeze(0)

4.4 Seq2seq

这是第一个不需要编码器RNN和解码器RNN具有相同隐藏尺寸的模型,但是编码器必须是双向的。 可以通过将所有出现的enc_dim * 2更改为enc_dim * 2(如果encoder_is_bidirectional否则为enc_dim)来消除此要求。
这个seq2seq和之前的两个类似,唯一的区别是编码器同时返回最终的隐藏状态(这是通过线性层的前向和后向编码器RNN的最终隐藏状态),以及解码器的初始隐藏状态 隐藏状态(即彼此堆叠的向前和向后隐藏状态)。 我们还需要确保将hidden和encoder_outputs传递给解码器。
步骤:

  • 创建输出张量来存放所有的预测\hat{y}
  • 源序列X被传入编码器来接受z,H
  • 解码器的初始隐藏状态被设置为上下文变量,即s_0=z=h_T
  • 第一个输入y_1被设置为一个batch的<sos>
  • 在解码的每个时间步里:
    • 输入初始tokeny_t,上一个隐藏状态s_{t-1},和所有编码器的输出H到解码器里
    • 输出预测\hat{y_{t+1}}和新的隐藏状态s_t
    • 是否需要teacher forcing
    def __init__(self, encoder, decoder, device):
        super().__init__()
        
        self.encoder = encoder
        self.decoder = decoder
        self.device = device
        
    def forward(self, src, trg, teacher_forcing_ratio = 0.5):
        
        #src = [src len, batch size]
        #trg = [trg len, batch size]
        #teacher_forcing_ratio is probability to use teacher forcing
        #e.g. if teacher_forcing_ratio is 0.75 we use teacher forcing 75% of the time
        
        batch_size = src.shape[1]
        trg_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim
        
        #tensor to store decoder outputs
        outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
        
        #encoder_outputs is all hidden states of the input sequence, back and forwards
        #hidden is the final forward and backward hidden states, passed through a linear layer
        encoder_outputs, hidden = self.encoder(src)
                
        #first input to the decoder is the <sos> tokens
        input = trg[0,:]
        
        for t in range(1, trg_len):
            
            #insert input token embedding, previous hidden state and all encoder hidden states
            #receive output tensor (predictions) and new hidden state
            output, hidden = self.decoder(input, hidden, encoder_outputs)
            
            #place predictions in a tensor holding predictions for each token
            outputs[t] = output
            
            #decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio
            
            #get the highest predicted token from our predictions
            top1 = output.argmax(1) 
            
            #if teacher forcing, use actual next token as next input
            #if not, use predicted token
            input = trg[t] if teacher_force else top1

        return outputs

5 训练

定义参数

OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
ENC_HID_DIM = 512
DEC_HID_DIM = 512
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5

attn = Attention(ENC_HID_DIM, DEC_HID_DIM)
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, DEC_DROPOUT, attn)

model = Seq2Seq(enc, dec, device).to(device)

初始化权重

def init_weights(m):
    for name, param in m.named_parameters():
        if 'weight' in name:
            nn.init.normal_(param.data, mean=0, std=0.01)
        else:
            nn.init.constant_(param.data, 0)
            
model.apply(init_weights)

计算可训练参数

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')

定义激活函数

optimizer = optim.Adam(model.parameters())

损失函数

TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]

criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX)

训练函数

    model.train()
    
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):
        
        src = batch.src
        trg = batch.trg
        
        optimizer.zero_grad()
        
        output = model(src, trg)
        
        #trg = [trg len, batch size]
        #output = [trg len, batch size, output dim]
        
        output_dim = output.shape[-1]
        
        output = output[1:].view(-1, output_dim)
        trg = trg[1:].view(-1)
        
        #trg = [(trg len - 1) * batch size]
        #output = [(trg len - 1) * batch size, output dim]
        
        loss = criterion(output, trg)
        
        loss.backward()
        
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        
        optimizer.step()
        
        epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

评估函数

def evaluate(model, iterator, criterion):
    
    model.eval()
    
    epoch_loss = 0
    
    with torch.no_grad():
    
        for i, batch in enumerate(iterator):

            src = batch.src
            trg = batch.trg

            output = model(src, trg, 0) #turn off teacher forcing

            #trg = [trg len, batch size]
            #output = [trg len, batch size, output dim]

            output_dim = output.shape[-1]
            
            output = output[1:].view(-1, output_dim)
            trg = trg[1:].view(-1)

            #trg = [(trg len - 1) * batch size]
            #output = [(trg len - 1) * batch size, output dim]

            loss = criterion(output, trg)

            epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)
def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs


N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):
    
    start_time = time.time()
    
    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)
    
    end_time = time.time()
    
    epoch_mins, epoch_secs = epoch_time(start_time, end_time)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut3-model.pt')
    
    print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')

6 结果

The model has 20,685,396 trainable parameters
Using device: cuda

TITAN V
Memory Usage:
Allocated: 0.1 GB
Cached:    0.1 GB
Epoch: 01 | Time: 0m 41s
    Train Loss: 5.030 | Train PPL: 152.919
     Val. Loss: 4.794 |  Val. PPL: 120.778
Epoch: 02 | Time: 0m 41s
    Train Loss: 4.094 | Train PPL:  59.991
     Val. Loss: 4.299 |  Val. PPL:  73.631
Epoch: 03 | Time: 0m 40s
    Train Loss: 3.321 | Train PPL:  27.691
     Val. Loss: 3.582 |  Val. PPL:  35.934
Epoch: 04 | Time: 0m 41s
    Train Loss: 2.714 | Train PPL:  15.093
     Val. Loss: 3.204 |  Val. PPL:  24.619
Epoch: 05 | Time: 0m 41s
    Train Loss: 2.302 | Train PPL:   9.991
     Val. Loss: 3.195 |  Val. PPL:  24.400
Epoch: 06 | Time: 0m 41s
    Train Loss: 1.964 | Train PPL:   7.131
     Val. Loss: 3.137 |  Val. PPL:  23.039
Epoch: 07 | Time: 0m 41s
    Train Loss: 1.719 | Train PPL:   5.578
     Val. Loss: 3.134 |  Val. PPL:  22.957
Epoch: 08 | Time: 0m 41s
    Train Loss: 1.498 | Train PPL:   4.474
     Val. Loss: 3.180 |  Val. PPL:  24.047
Epoch: 09 | Time: 0m 41s
    Train Loss: 1.323 | Train PPL:   3.753
     Val. Loss: 3.234 |  Val. PPL:  25.378
Epoch: 10 | Time: 0m 41s
    Train Loss: 1.173 | Train PPL:   3.231
     Val. Loss: 3.320 |  Val. PPL:  27.656
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351