[動手學深度學習-PyTorch版]-2.預備知識

2.1 環境配置

本節簡單介紹一些必要的軟件的安裝與配置,由于不同機器軟硬件配置不同,所以不詳述,遇到問題請善用Google。

2.1.1 Anaconda

Anaconda是Python的一個開源發行版本,主要面向科學計算。我們可以簡單理解為,Anaconda是一個預裝了很多我們用的到或用不到的第三方庫的Python。而且相比于大家熟悉的pip install命令,Anaconda中增加了conda install命令。當你熟悉了Anaconda以后會發現,conda install會比pip install更方便一些。 強烈建議先去看看最省心的Python版本和第三方庫管理——初探Anaconda初學 Python 者自學 Anaconda 的正確姿勢-猴子的回答

總的來說,我們應該完成以下幾步:

  • 根據操作系統下載并安裝Anaconda(或者mini版本Miniconda)并學會常用的幾個conda命令,例如如何管理python環境、如何安裝卸載包等;
  • Anaconda安裝成功之后,我們需要修改其包管理鏡像為國內源,這樣以后安裝包時就會快一些。

2.1.2 Jupyter

在沒有notebook之前,在IT領域是這樣工作的:在普通的 Python shell 或者在IDE(集成開發環境)如Pycharm中寫代碼,然后在word中寫文檔來說明你的項目。這個過程很繁瑣,通常是寫完代碼,再寫文檔的時候我還的重頭回顧一遍代碼。最蛋疼的地方在于,有些數據分析的中間結果,還得重新跑代碼,然后把結果弄到文檔里給客戶看。有了notebook之后,世界突然美好了許多,因為notebook可以直接在代碼旁寫出敘述性文檔,而不是另外編寫單獨的文檔。也就是它可以能將代碼、文檔等這一切集中到一處,讓用戶一目了然。如下圖所示。

image

Jupyter Notebook 已迅速成為數據分析,機器學習的必備工具。因為它可以讓數據分析師集中精力向用戶解釋整個分析過程。

我們參考jupyter notebook-猴子的回答進行jupyter notebook及常用包(例如環境自動關聯包nb_conda)的安裝。

安裝好后,我們使用以下命令打開一個jupyter notebook:

jupyter notebook 

這時在瀏覽器打開 http://localhost:8888 (通常會自動打開)位于當前目錄的jupyter服務。

2.1.3 PyTorch

由于本文需要用到PyTorch框架,所以還需要安裝PyTorch(后期必不可少地會使用GPU,所以安裝GPU版本的)。直接去PyTorch官網找到自己的軟硬件對應的安裝命令即可(這里不得不吹一下PyTorch的官方文檔,從安裝到入門,深入淺出,比tensorflow不知道高到哪里去了)。安裝好后使用以下命令可查看安裝的PyTorch及版本號。

conda list | grep torch

2.1.4 其他

此外還可以安裝python最好用的IDE PyCharm,專業版的應該是需要收費的,但學生用戶可以申請免費使用(傳送門),或者直接用免費的社區版。

如果不喜歡用IDE也可以選擇編輯器,例如VSCode等。

本節與原文有很大不同,原文傳送門

2.2 數據操作

在深度學習中,我們通常會頻繁地對數據進行操作。作為動手學深度學習的基礎,本節將介紹如何對內存中的數據進行操作。

在PyTorch中,torch.Tensor是存儲和變換數據的主要工具。如果你之前用過NumPy,你會發現Tensor和NumPy的多維數組非常類似。然而,Tensor提供GPU計算和自動求梯度等更多功能,這些使Tensor更加適合深度學習。

"tensor"這個單詞一般可譯作“張量”,張量可以看作是一個多維數組。標量可以看作是0維張量,向量可以看作1維張量,矩陣可以看作是二維張量。

2.2.1 創建Tensor

我們先介紹Tensor的最基本功能,即Tensor的創建。

首先導入PyTorch:

import torch

然后我們創建一個5x3的未初始化的Tensor

x = torch.empty(5, 3)
print(x)

輸出:

tensor([[ 0.0000e+00,  1.5846e+29,  0.0000e+00],
        [ 1.5846e+29,  5.6052e-45,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00],
        [ 0.0000e+00,  1.5846e+29, -2.4336e+02]])

創建一個5x3的隨機初始化的Tensor:

x = torch.rand(5, 3)
print(x)

輸出:

tensor([[0.4963, 0.7682, 0.0885],
        [0.1320, 0.3074, 0.6341],
        [0.4901, 0.8964, 0.4556],
        [0.6323, 0.3489, 0.4017],
        [0.0223, 0.1689, 0.2939]])

創建一個5x3的long型全0的Tensor:

x = torch.zeros(5, 3, dtype=torch.long)
print(x)

輸出:

tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]])

還可以直接根據數據創建:

x = torch.tensor([5.5, 3])
print(x)

輸出:

tensor([5.5000, 3.0000])

還可以通過現有的Tensor來創建,此方法會默認重用輸入Tensor的一些屬性,例如數據類型,除非自定義數據類型。

x = x.new_ones(5, 3, dtype=torch.float64)  # 返回的tensor默認具有相同的torch.dtype和torch.device
print(x)

x = torch.randn_like(x, dtype=torch.float) # 指定新的數據類型
print(x) 

輸出:

tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]], dtype=torch.float64)
tensor([[ 0.6035,  0.8110, -0.0451],
        [ 0.8797,  1.0482, -0.0445],
        [-0.7229,  2.8663, -0.5655],
        [ 0.1604, -0.0254,  1.0739],
        [ 2.2628, -0.9175, -0.2251]])

我們可以通過shape或者size()來獲取Tensor的形狀:

print(x.size())
print(x.shape)

輸出:

torch.Size([5, 3])
torch.Size([5, 3])

注意:返回的torch.Size其實就是一個tuple, 支持所有tuple的操作。

還有很多函數可以創建Tensor,去翻翻官方API就知道了,下表給了一些常用的作參考。

函數 功能
Tensor(*sizes) 基礎構造函數
tensor(data,) 類似np.array的構造函數
ones(*sizes) 全1Tensor
zeros(*sizes) 全0Tensor
eye(*sizes) 對角線為1,其他為0
arange(s,e,step) 從s到e,步長為step
linspace(s,e,steps) 從s到e,均勻切分成steps份
rand/randn(*sizes) 均勻/標準分布
normal(mean,std)/uniform(from,to) 正態分布/均勻分布
randperm(m) 隨機排列

這些創建方法都可以在創建的時候指定數據類型dtype和存放device(cpu/gpu)。

2.2.2 操作

本小節介紹Tensor的各種操作。

算術操作

在PyTorch中,同一種操作可能有很多種形式,下面用加法作為例子。

  • 加法形式一

      y = torch.rand(5, 3)
      print(x + y)
    
  • 加法形式二

      print(torch.add(x, y))
    

    還可指定輸出:

      result = torch.empty(5, 3)
      torch.add(x, y, out=result)
      print(result)
    
  • 加法形式三、inplace

      # adds x to y
      y.add_(x)
      print(y)
    

    注:PyTorch操作inplace版本都有后綴_, 例如x.copy_(y), x.t_()

以上幾種形式的輸出均為:

tensor([[ 1.3967,  1.0892,  0.4369],
        [ 1.6995,  2.0453,  0.6539],
        [-0.1553,  3.7016, -0.3599],
        [ 0.7536,  0.0870,  1.2274],
        [ 2.5046, -0.1913,  0.4760]])

索引

我們還可以使用類似NumPy的索引操作來訪問Tensor的一部分,需要注意的是:索引出來的結果與原數據共享內存,也即修改一個,另一個會跟著修改。

y = x[0, :]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了

輸出:

tensor([1.6035, 1.8110, 0.9549])
tensor([1.6035, 1.8110, 0.9549])

除了常用的索引選擇數據之外,PyTorch還提供了一些高級的選擇函數:

函數 功能
index_select(input, dim, index) 在指定維度dim上選取,比如選取某些行、某些列
masked_select(input, mask) 例子如上,a[a>0],使用ByteTensor進行選取
nonzero(input) 非0元素的下標
gather(input, dim, index) 根據index,在dim維度上選取數據,輸出的size與index一樣

這里不詳細介紹,用到了再查官方文檔。

改變形狀

view()來改變Tensor的形狀:

y = x.view(15)
z = x.view(-1, 5)  # -1所指的維度可以根據其他維度的值推出來
print(x.size(), y.size(), z.size())

輸出:

torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])

注意view()返回的新Tensor與源Tensor雖然可能有不同的size,但是是共享data的,也即更改其中的一個,另外一個也會跟著改變。(顧名思義,view僅僅是改變了對這個張量的觀察角度,內部數據并未改變)

x += 1
print(x)
print(y) # 也加了1

輸出:

tensor([[1.6035, 1.8110, 0.9549],
        [1.8797, 2.0482, 0.9555],
        [0.2771, 3.8663, 0.4345],
        [1.1604, 0.9746, 2.0739],
        [3.2628, 0.0825, 0.7749]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
        1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])

所以如果我們想返回一個真正新的副本(即不共享data內存)該怎么辦呢?Pytorch還提供了一個reshape()可以改變形狀,但是此函數并不能保證返回的是其拷貝,所以不推薦使用。推薦先用clone創造一個副本然后再使用view參考此處

x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)

輸出:

tensor([[ 0.6035,  0.8110, -0.0451],
        [ 0.8797,  1.0482, -0.0445],
        [-0.7229,  2.8663, -0.5655],
        [ 0.1604, -0.0254,  1.0739],
        [ 2.2628, -0.9175, -0.2251]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
        1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])

使用clone還有一個好處是會被記錄在計算圖中,即梯度回傳到副本時也會傳到源Tensor

另外一個常用的函數就是item(), 它可以將一個標量Tensor轉換成一個Python number:

x = torch.randn(1)
print(x)
print(x.item())

輸出:

tensor([2.3466])
2.3466382026672363

線性代數

另外,PyTorch還支持一些線性函數,這里提一下,免得用起來的時候自己造輪子,具體用法參考官方文檔。如下表所示:

函數 功能
trace 對角線元素之和(矩陣的跡)
diag 對角線元素
triu/tril 矩陣的上三角/下三角,可指定偏移量
mm/bmm 矩陣乘法,batch的矩陣乘法
addmm/addbmm/addmv/addr/baddbmm.. 矩陣運算
t 轉置
dot/cross 內積/外積
inverse 求逆矩陣
svd 奇異值分解

PyTorch中的Tensor支持超過一百種操作,包括轉置、索引、切片、數學運算、線性代數、隨機數等等,可參考官方文檔

2.2.3 廣播機制

前面我們看到如何對兩個形狀相同的Tensor做按元素運算。當對兩個形狀不同的Tensor按元素運算時,可能會觸發廣播(broadcasting)機制:先適當復制元素使這兩個Tensor形狀相同后再按元素運算。例如:

x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)

輸出:

tensor([[1, 2]])
tensor([[1],
        [2],
        [3]])
tensor([[2, 3],
        [3, 4],
        [4, 5]])

由于xy分別是1行2列和3行1列的矩陣,如果要計算x + y,那么x中第一行的2個元素被廣播(復制)到了第二行和第三行,而y中第一列的3個元素被廣播(復制)到了第二列。如此,就可以對2個3行2列的矩陣按元素相加。

2.2.4 運算的內存開銷

前面說了,索引操作是不會開辟新內存的,而像y = x + y這樣的運算是會新開內存的,然后將y指向新內存。為了演示這一點,我們可以使用Python自帶的id函數:如果兩個實例的ID一致,那么它們所對應的內存地址相同;反之則不同。

x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x
print(id(y) == id_before) # False 

如果想指定結果到原來的y的內存,我們可以使用前面介紹的索引來進行替換操作。在下面的例子中,我們把x + y的結果通過[:]寫進y對應的內存中。

x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y[:] = y + x
print(id(y) == id_before) # True

我們還可以使用運算符全名函數中的out參數或者自加運算符+=(也即add_())達到上述效果,例如torch.add(x, y, out=y)y += x(y.add_(x))。

x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
torch.add(x, y, out=y) # y += x, y.add_(x)
print(id(y) == id_before) # True

注:雖然view返回的Tensor與源Tensor是共享data的,但是依然是一個新的Tensor(因為Tensor除了包含data外還有一些其他屬性),二者id(內存地址)并不一致。

2.2.5 Tensor和NumPy相互轉換

我們很容易用numpy()from_numpy()Tensor和NumPy中的數組相互轉換。但是需要注意的一點是: 這兩個函數所產生的的Tensor和NumPy中的數組共享相同的內存(所以他們之間的轉換很快),改變其中一個時另一個也會改變!!!

還有一個常用的將NumPy中的array轉換成Tensor的方法就是torch.tensor(), 需要注意的是,此方法總是會進行數據拷貝(就會消耗更多的時間和空間),所以返回的Tensor和原來的數據不再共享內存。

Tensor轉NumPy

使用numpy()Tensor轉換成NumPy數組:

a = torch.ones(5)
b = a.numpy()
print(a, b)

a += 1
print(a, b)
b += 1
print(a, b)

輸出:

tensor([1., 1., 1., 1., 1.]) [1\. 1\. 1\. 1\. 1.]
tensor([2., 2., 2., 2., 2.]) [2\. 2\. 2\. 2\. 2.]
tensor([3., 3., 3., 3., 3.]) [3\. 3\. 3\. 3\. 3.]

NumPy數組轉Tensor

使用from_numpy()將NumPy數組轉換成Tensor:

import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)

a += 1
print(a, b)
b += 1
print(a, b)

輸出:

[1\. 1\. 1\. 1\. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
[2\. 2\. 2\. 2\. 2.] tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
[3\. 3\. 3\. 3\. 3.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)

所有在CPU上的Tensor(除了CharTensor)都支持與NumPy數組相互轉換。

此外上面提到還有一個常用的方法就是直接用torch.tensor()將NumPy數組轉換成Tensor,需要注意的是該方法總是會進行數據拷貝,返回的Tensor和原來的數據不再共享內存。

c = torch.tensor(a)
a += 1
print(a, c)

輸出

[4\. 4\. 4\. 4\. 4.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)

2.2.6 Tensor on GPU

用方法to()可以將Tensor在CPU和GPU(需要硬件支持)之間相互移動。

# 以下代碼只有在PyTorch GPU版本上才會執行
if torch.cuda.is_available():
    device = torch.device("cuda")          # GPU
    y = torch.ones_like(x, device=device)  # 直接創建一個在GPU上的Tensor
    x = x.to(device)                       # 等價于 .to("cuda")
    z = x + y
    print(z)
    print(z.to("cpu", torch.double))       # to()還可以同時更改數據類型

注: 本文主要參考PyTorch官方文檔此處,與原書同一節有很大不同。

2.3 自動求梯度

在深度學習中,我們經常需要對函數求梯度(gradient)。PyTorch提供的autograd包能夠根據輸入和前向傳播過程自動構建計算圖,并執行反向傳播。本節將介紹如何使用autograd包來進行自動求梯度的有關操作。

2.3.1 概念

上一節介紹的Tensor是這個包的核心類,如果將其屬性.requires_grad設置為True,它將開始追蹤(track)在其上的所有操作(這樣就可以利用鏈式法則進行梯度傳播了)。完成計算后,可以調用.backward()來完成所有梯度計算。此Tensor的梯度將累積到.grad屬性中。

注意在y.backward()時,如果y是標量,則不需要為backward()傳入任何參數;否則,需要傳入一個與y同形的Tensor。解釋見 2.3.2 節。

如果不想要被繼續追蹤,可以調用.detach()將其從追蹤記錄中分離出來,這樣就可以防止將來的計算被追蹤,這樣梯度就傳不過去了。此外,還可以用with torch.no_grad()將不想被追蹤的操作代碼塊包裹起來,這種方法在評估模型的時候很常用,因為在評估模型時,我們并不需要計算可訓練參數(requires_grad=True)的梯度。

Function是另外一個很重要的類。TensorFunction互相結合就可以構建一個記錄有整個計算過程的有向無環圖(DAG)。每個Tensor都有一個.grad_fn屬性,該屬性即創建該TensorFunction, 就是說該Tensor是不是通過某些運算得到的,若是,則grad_fn返回一個與這些運算相關的對象,否則是None。

下面通過一些例子來理解這些概念。

2.3.2 Tensor

創建一個Tensor并設置requires_grad=True:

x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)

輸出:

tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
None

再做一下運算操作:

y = x + 2
print(y)
print(y.grad_fn)

輸出:

tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>

注意x是直接創建的,所以它沒有grad_fn, 而y是通過一個加法操作創建的,所以它有一個為<AddBackward>grad_fn

像x這種直接創建的稱為葉子節點,葉子節點對應的grad_fnNone

print(x.is_leaf, y.is_leaf) # True False

再來點復雜度運算操作:

z = y * y * 3
out = z.mean()
print(z, out)

輸出:

tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=<MeanBackward1>)

通過.requires_grad_()來用in-place的方式改變requires_grad屬性:

a = torch.randn(2, 2) # 缺失情況下默認 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)

輸出:

False
True
<SumBackward0 object at 0x118f50cc0>

2.3.3 梯度

因為out是一個標量,所以調用backward()時不需要指定求導變量:

out.backward() # 等價于 out.backward(torch.tensor(1.))

我們來看看out關于x的梯度 [Math Processing Error]<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><annotation encoding="application/x-tex">\frac{d(out)}{dx}</annotation></semantics></math>dxd(out):

print(x.grad)

輸出:

tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])
image.png

image.png

注意:grad在反向傳播過程中是累加的(accumulated),這意味著每一次運行反向傳播,梯度都會累加之前的梯度,所以一般在反向傳播之前需把梯度清零。

# 再來反向傳播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)

輸出:

tensor([[5.5000, 5.5000],
        [5.5000, 5.5000]])
tensor([[1., 1.],
        [1., 1.]])

現在我們解釋2.3.1節留下的問題,為什么在y.backward()時,如果y是標量,則不需要為backward()傳入任何參數;否則,需要傳入一個與y同形的Tensor? 簡單來說就是為了避免向量(甚至更高維張量)對張量求導,而轉換成標量對張量求導。舉個例子,假設形狀為 m x n 的矩陣 X 經過運算得到了 p x q 的矩陣 Y,Y 又經過運算得到了 s x t 的矩陣 Z。那么按照前面講的規則,dZ/dY 應該是一個 s x t x p x q 四維張量,dY/dX 是一個 p x q x m x n的四維張量。問題來了,怎樣反向傳播?怎樣將兩個四維張量相乘???這要怎么乘???就算能解決兩個四維張量怎么乘的問題,四維和三維的張量又怎么乘?導數的導數又怎么求,這一連串的問題,感覺要瘋掉…… 為了避免這個問題,我們不允許張量對張量求導,只允許標量對張量求導,求導結果是和自變量同形的張量。所以必要時我們要把張量通過將所有張量的元素加權求和的方式轉換為標量,舉個例子,假設y由自變量x計算而來,w是和y同形的張量,則y.backward(w)的含義是:先計算l = torch.sum(y * w),則l是個標量,然后求l對自變量x的導數。 參考

來看一些實際例子。

x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)

輸出:

tensor([[2., 4.],
        [6., 8.]], grad_fn=<ViewBackward>)

現在 z 不是一個標量,所以在調用backward時需要傳入一個和z同形的權重向量進行加權求和得到一個標量。

v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad)

輸出:

tensor([2.0000, 0.2000, 0.0200, 0.0020])

注意,x.grad是和x同形的張量。

再來看看中斷梯度追蹤的例子:

x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2 
with torch.no_grad():
    y2 = x ** 3
y3 = y1 + y2

print(x.requires_grad)
print(y1, y1.requires_grad) # True
print(y2, y2.requires_grad) # False
print(y3, y3.requires_grad) # True

輸出:

True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<ThAddBackward>) True

可以看到,上面的y2是沒有grad_fn而且y2.requires_grad=False的,而y3是有grad_fn的。如果我們將y3x求梯度的話會是多少呢?

y3.backward()
print(x.grad)

輸出:

tensor(2.)
image.png

上面提到,y2.requires_grad=False,所以不能調用 y2.backward(),會報錯:

RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

此外,如果我們想要修改tensor的數值,但是又不希望被autograd記錄(即不會影響反向傳播),那么我么可以對tensor.data進行操作。

x = torch.ones(1,requires_grad=True)

print(x.data) # 還是一個tensor
print(x.data.requires_grad) # 但是已經是獨立于計算圖之外

y = 2 * x
x.data *= 100 # 只改變了值,不會記錄在計算圖,所以不會影響梯度傳播

y.backward()
print(x) # 更改data的值也會影響tensor的值
print(x.grad)

輸出:

tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])

注: 本文主要參考PyTorch官方文檔,與原書同一節有很大不同。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容