- 原文地址:Neural Networks from Scratch (in R)
- 原文做者:Ilia Karmanov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:CACppuccino
- 校對者:Isvih
這篇文章是針對那些有統計或者經濟學背景的人們,幫助他們經過 R 語言上的 Scratch 平臺更好地學習和理解機器學習知識。html
Andrej Karpathy 在 CS231n 課程中這樣說道 :前端
「咱們有意識地在設計課程的時候,於反向傳播算法的編程做業中包含了對最底層的數據的計算要求。學生們須要在原始的 numpy 庫中使數據在各層中正向、反向傳播。一些學生於是不免在課程的留言板上抱怨(這些複雜的計算)」react
若是框架已經爲你完成了反向傳播算法(BP 算法)的計算,你又何苦折磨本身而不去探尋更多有趣的深度學習問題呢?android
import keras
model = Sequential()
model.add(Dense(512, activation=’relu’, input_shape=(784,)))
model.add(Dense(10, activation=’softmax’))
model.compile(loss=’categorical_crossentropy’, optimizer=RMSprop())
model.fit()複製代碼
Karpathy教授,將「智力上的好奇」或者「你可能想要晚些提高核心算法」的論點抽象出來,認爲計算其實是一種泄漏抽象(譯者注:「抽象泄漏」是軟件開發時,本應隱藏實現細節的抽象化不可避免地暴露出底層細節與侷限性。抽象泄露是棘手的問題,由於抽象化原本目的就是向用戶隱藏沒必要要公開的細節--維基百科):ios
「人們很容易陷入這樣的誤區中-認爲你能夠簡單地將任意的神經層組合在一塊兒而後反向傳播算法會‘令它們本身在你的數據上工做起來’。」git
所以,我寫這篇文章的目的有兩層:github
理解神經網絡背後的抽象泄漏(經過在 Scratch 平臺上操做),而這些東西的重要性偏偏是我開始所忽略的。這樣若是個人模型沒有達到預期的學習效果,我能夠更好地解決問題,而不是盲目地改變優化方案(甚至更換學習框架)。算法
一個深度神經網絡(DNN),一旦被拆分紅塊,對於 AI 領域以外的人們也不再是一個黑箱了。相反,對於大多數有基本的統計背景的人來講,是一個個很是熟悉的話題的組合。我相信他們只須要學習不多的一些(只是那些如何將這一塊塊知識組合一塊兒)知識就能夠在一個全新的領域得到不錯的洞察力。編程
從線性迴歸開始,藉着 R-notebook,經過解決一系列的數學和編程問題直至瞭解深度神經網絡(DNN)。但願可以藉此展現出來,你所需學習的新知識其實只有不多的一部分。後端
筆記
github.com/ilkarman/De…
github.com/ilkarman/De…
github.com/ilkarman/De…
github.com/ilkarman/De…
在 R 中解決最小二乘法的計算器的閉包解決方案只需以下幾行:
# Matrix of explanatory variables
X <- as.matrix(X)
# Add column of 1s for intercept coefficient
intcpt <- rep(1, length(y))
# Combine predictors with intercept
X <- cbind(intcpt, X)
# OLS (closed-form solution)
beta_hat <- solve(t(X) %*% X) %*% t(X) %*% y複製代碼
變量 beta_hat 所造成的向量包含的數值,定義了咱們的「機器學習模型」。線性迴歸是用來預測一個連續的變量的(例如:這架飛機會延誤多久)。在預測分類的時候(例如:這架飛機會延誤嗎-會/不會),咱們但願咱們的預測可以落在0到1之間,這樣咱們能夠將其轉換爲各個種類的事件發生的可能性(根據所給的數據)。
當咱們只有兩個互斥的結果時咱們將使用一個二項邏輯迴歸。當候選結果(或者分類)多於兩個時,即多項互斥(例如:這架飛機延誤時間可能在5分鐘內、5-10分鐘或多於10分鐘),咱們將使用多項邏輯迴歸(或者「Softmax 迴歸」)(譯者注:Softmax 函數是邏輯函數的一種推廣,更多知識見知乎)。在這種狀況下許多類別不是互斥的(例如:這篇文章中的「R」,「神經網絡」和「統計學」),咱們能夠採用二項式邏輯迴歸(譯者注:不是二項邏輯迴歸)。
另外,咱們也能夠用梯度降低(GD)這種迭代法來替代咱們上文提到的閉包方法。整個過程以下:
GD 僅僅使用了 Jacobian 矩陣 (而不是 Hessian 矩陣),不過咱們知道, 當咱們的損失函數爲凸函數時,全部的極小值即(局部最小值)爲(全局)最小值,所以 GD 總可以收斂至全局最小值。
線性迴歸中所用的損失函數是均方偏差函數:
要使用 GD 方法咱們只須要找出 beta_hat 的偏導數(即 'delta'/梯度)
在 R 中實現方法以下:
# Start with a random guess
beta_hat <- matrix(0.1, nrow=ncol(X_mat))
# Repeat below for N-iterations
for (j in 1:N)
{
# Calculate the cost/error (y_guess - y_truth)
residual <- (X_mat %*% beta_hat) - y
# Calculate the gradient at that point
delta <- (t(X_mat) %*% residual) * (1/nrow(X_mat))
# Move guess in opposite direction of gradient
beta_hat <- beta_hat - (lr*delta)
}複製代碼
200次的迭代以後咱們會獲得和閉包方法同樣的梯度與參數。除了這表明着咱們的進步意外(咱們使用了 GD),這個迭代方法在當閉包方法因矩陣過大,而沒法計算矩陣的逆的時候,也很是有用(由於有內存的限制)。
邏輯迴歸即一種用來解決二項分類的線性迴歸方法。它與標準的線性迴歸主要的兩種不一樣在於:
其它的都保持不變。
咱們能夠像這樣計算咱們的激活函數:
sigmoid <- function(z){1.0/(1.0+exp(-z))}複製代碼
咱們能夠在 R 中這樣建立對數似然函數:
log_likelihood <- function(X_mat, y, beta_hat)
{
scores <- X_mat %*% beta_hat
ll <- (y * scores) - log(1+exp(scores))
sum(ll)
}複製代碼
這個損失函數(邏輯損失或對數損失函數)也叫作交叉熵損失。交叉熵損失根本上來說是對「意外」的一種測量,而且會成爲全部接下來的模型的基礎,因此值得多花一些時間。
若是咱們還像之前同樣創建最小平方損失函數,因爲咱們目前擁有的是一個非線性激活函數(sigmoid),那麼損失函數將因再也不是凸函數而使優化變得困難。
咱們能夠爲兩個分類設立本身的損失函數。當 y=1 時,咱們但願咱們的損失函數值在預測值接近0的時候變得很是高,在接近1的時候變得很是低。當 y=0 時,咱們所指望的與以前偏偏相反。這致使了咱們有了以下的損失函數:
這裏的損失函數中的 delta 與咱們以前的線性迴歸中的 delta 很是類似。惟一的不一樣在於咱們在這裏將 sigmoid 函數也應用在了預測之中。這意味着邏輯迴歸中的梯度降低函數也會看起來很類似:
logistic_reg <- function(X, y, epochs, lr)
{
X_mat <- cbind(1, X)
beta_hat <- matrix(1, nrow=ncol(X_mat))
for (j in 1:epochs)
{
# For a linear regression this was:
# 1*(X_mat %*% beta_hat) - y
residual <- sigmoid(X_mat %*% beta_hat) - y
# Update weights with gradient descent
delta <- t(X_mat) %*% as.matrix(residual, ncol=nrow(X_mat)) * (1/nrow(X_mat))
beta_hat <- beta_hat - (lr*delta)
}
# Print log-likliehood
print(log_likelihood(X_mat, y, beta_hat))
# Return
beta_hat
}複製代碼
邏輯迴歸的推廣即爲多項邏輯迴歸(也稱爲 ‘softmax 函數’),是對兩項以上的分類進行預測的。我還沒有在 R 中創建這個例子,由於下一步的神經網絡中也有一些東西簡化以後與之類似,然而爲了完整起見,若是你仍然想要建立它的話,我仍是要強調一下這裏主要的不一樣。
首先,咱們再也不用 sigmoid 函數來說咱們所得的值壓縮在 0 至 1 之間:
咱們用 softmax 函數來將 n 個值的和壓縮至 1:
這樣意味着每一個類別所得的值,能夠根據所給的條件,被轉化爲該類的機率。同時也意味着當咱們但願提升某一分類的權重來提升它所得到的機率的時候,其它分類的出現機率會有所降低。也就是說,咱們的各個類別是互斥的。
其次,咱們使用一個更加通用的交叉熵損失函數:
要想知道爲何-記住對於二項分類(如以前的例子)咱們有兩個類別:j = 2,在每一個類別是互斥的,a1 + a2 = 1 且 y 是一位有效編碼(one-hot)因此 y1+y2=1,咱們能夠將通用公式重寫爲:
(譯者注:one-hot是將分類的特徵轉化爲更加適合分類和迴歸算法的數據格式(Quora-Håkon Hapnes Strand),中文資料可見此)
這與咱們剛開始的等式是相同的。然而,咱們如今將 j=2 的條件放寬。這裏的交叉熵損失函數能夠被看出來有着與二項分類的邏輯輸出的交叉熵有着相同的梯度。
然而,即便梯度有着相同的公式,也會由於激活函數代入了不一樣的值而不同(用了 softmax 而不是邏輯中的 sigmoid)。
在大多數的深度學習框架中,你能夠選擇‘二項交叉熵(binary_crossentropy)’或者‘分類交叉熵(categorical_crossentropy)’損失函數。這取決於你的最後一層神經包含的是 sigmoid 仍是 softmax 激活函數,相對應着,你能夠選擇‘二項交叉熵(binary_crossentropy)’或者‘分類交叉熵(categorical_crossentropy)’。而因爲梯度相同,神經網絡的訓練並不會被影響,然而所獲得的損失(或評測值)會因爲搞混它們而錯誤。
之因此要涉及到 softmax 是由於大多數的神經網絡,會在各個類別互斥的時候,用 softmax 層做爲最後一層(讀出層),用多項交叉熵(也叫分類交叉熵)損失函數,而不是用 sigmoid 函數搭配二項交叉熵損失函數。儘管多項 sigmoid 也能夠用於多類別分類(而且會被用於下個例子中),但這整體上僅用於多項不互斥的時候。有了 softmax 做爲輸出,因爲輸出的和被限制爲 1,咱們能夠直接將輸出轉化爲機率。
一個神經網絡能夠被看做爲一系列的邏輯迴歸堆疊在一塊兒。這意味着咱們能夠說,一個邏輯迴歸其實是一個(帶有 sigmoid 激活函數)無隱藏層的神經網絡。
隱藏層,使神經網絡具備非線性且致使了用於通用近似定理所描述的特性。該定理聲明,一個神經網絡和一個隱藏層能夠逼近任何線性或非線性的函數。而隱藏層的數量能夠擴展至上百層。
若是將神經網絡看做兩個東西的結合會頗有用:1)不少的邏輯迴歸堆疊在一塊兒造成‘特徵生成器’ 2)一個 softmax 迴歸函數構成的單個讀出層。近來深度學習的成功可歸功於‘特徵生成器’。例如:在之前的計算機視覺領域,咱們須要痛苦地聲明咱們須要找到各類長方形,圓形,顏色和結合方式(與經濟學家們如何決定哪些相互做用須要用於線性迴歸中類似)。如今,隱藏層是對決定哪一個特徵(哪一個‘相互做用’)須要提取的優化器。不少的深度學習其實是經過用一個訓練好的模型,去掉讀出層,而後用那些特徵做爲輸入(或者是促進決策樹(boosted decision-trees))來生成的。
隱藏層同時也意味着咱們的損失函數在參數中不是一個凸函數,咱們不可以經過一個平滑的山坡來到達底部。咱們會用隨機梯度降低(SGD)而不是梯度降低(GD),不像咱們以前在邏輯迴歸中作的同樣,這樣基本上在每一次小批量(mini-batch)(比觀察總數小不少)被在神經網絡中傳播後都會重編觀察(隨機)並更新梯度。這裏有不少 SGD 的替代方法,Sebastian Ruder 爲咱們作了不少工做。我認爲這確實是個迷人的話題,不過卻超出這篇博文所討論的範圍了,很遺憾。簡要來說,大多數優化方法是一階的(包括 SGD,Adam,RMSprop和 Adagrad)由於計算二階函數的計算難度太高。然而,一些一階方法有一個固定的學習頻率(SGD)而有一些擁有適應性學習頻率(Adam),這意味着咱們經過成爲損失函數所更新權重的‘數量’-將會在開始有巨大的變化而隨着咱們接近目標而逐漸變小。
須要弄清楚的一點是,最小化訓練數據上的損失並不是咱們的主要目標-理論上咱們但願最小化‘不可見的’(測試)數據的損失;所以全部的優化方法都表明着已經一種假設之下,即訓練數據的的低損失會以一樣的(損失)分佈推廣至‘新’的數據。這意味着咱們可能更青睞於一個有着更高的訓練數據損失的神經網絡;由於它在驗證數據上的損失很低(即那些不曾被用於訓練的數據)-咱們則會說該神經網絡在這種狀況下‘過分擬合’了。這裏有一些近期的論文聲稱,他們發現了不少很尖的最小值點,因此適應性優化方法並不像 SGD 同樣可以很好的推廣。(譯者注:即算法在一些驗證數據中表現地出奇的差)
以前咱們須要將梯度反向傳播一層,如今同樣,咱們也須要將其反向傳播過全部的隱藏層。關於反向傳播算法的解釋,已經超出了本文的範圍,然而理解這個算法倒是十分必要的。這裏有一些不錯的資源可能對各位有所幫助。
咱們如今能夠在 Scratch 平臺上用 R 經過四個函數創建一個神經網絡了。
咱們首先初始化權重:
neuralnetwork <- function(sizes, training_data, epochs, mini_batch_size, lr, C, verbose=FALSE, validation_data=training_data)
因爲咱們將參數進行了複雜的結合,咱們不能簡單地像之前同樣將它們初始化爲 1 或 0,神經網絡會所以而在計算過程當中卡住。爲了防止這種狀況,咱們採用高斯分佈(不過就像那些優化方法同樣,這也有許多其餘的方法):
biases <- lapply(seq_along(listb), function(idx){
r <- listb[[idx]]
matrix(rnorm(n=r), nrow=r, ncol=1)
})
weights <- lapply(seq_along(listb), function(idx){
c <- listw[[idx]]
r <- listb[[idx]]
matrix(rnorm(n=r*c), nrow=r, ncol=c)
})複製代碼
SGD <- function(training_data, epochs, mini_batch_size, lr, C, sizes, num_layers, biases, weights,verbose=FALSE, validation_data)
{
# Every epoch
for (j in 1:epochs){
# Stochastic mini-batch (shuffle data)
training_data <- sample(training_data)
# Partition set into mini-batches
mini_batches <- split(training_data,
ceiling(seq_along(training_data)/mini_batch_size))
# Feed forward (and back) all mini-batches
for (k in 1:length(mini_batches)) {
# Update biases and weights
res <- update_mini_batch(mini_batches[[k]], lr, C, sizes, num_layers, biases, weights)
biases <- res[[1]]
weights <- res[[-1]]
}
}
# Return trained biases and weights
list(biases, weights)
}複製代碼
做爲 SGD 方法的一部分,咱們更新了
update_mini_batch <- function(mini_batch, lr, C, sizes, num_layers, biases, weights)
{
nmb <- length(mini_batch)
listw <- sizes[1:length(sizes)-1]
listb <- sizes[-1]
# Initialise updates with zero vectors (for EACH mini-batch)
nabla_b <- lapply(seq_along(listb), function(idx){
r <- listb[[idx]]
matrix(0, nrow=r, ncol=1)
})
nabla_w <- lapply(seq_along(listb), function(idx){
c <- listw[[idx]]
r <- listb[[idx]]
matrix(0, nrow=r, ncol=c)
})
# Go through mini_batch
for (i in 1:nmb){
x <- mini_batch[[i]][[1]]
y <- mini_batch[[i]][[-1]]
# Back propagation will return delta
# Backprop for each observation in mini-batch
delta_nablas <- backprop(x, y, C, sizes, num_layers, biases, weights)
delta_nabla_b <- delta_nablas[[1]]
delta_nabla_w <- delta_nablas[[-1]]
# Add on deltas to nabla
nabla_b <- lapply(seq_along(biases),function(j)
unlist(nabla_b[[j]])+unlist(delta_nabla_b[[j]]))
nabla_w <- lapply(seq_along(weights),function(j)
unlist(nabla_w[[j]])+unlist(delta_nabla_w[[j]]))
}
# After mini-batch has finished update biases and weights:
# i.e. weights = weights - (learning-rate/numbr in batch)*nabla_weights
# Opposite direction of gradient
weights <- lapply(seq_along(weights), function(j)
unlist(weights[[j]])-(lr/nmb)*unlist(nabla_w[[j]]))
biases <- lapply(seq_along(biases), function(j)
unlist(biases[[j]])-(lr/nmb)*unlist(nabla_b[[j]]))
# Return
list(biases, weights)
}複製代碼
咱們用來計算 delta 的算法是反向傳播算法。
在這個例子中咱們使用交叉熵損失函數,產生了如下的梯度:
cost_delta <- function(method, z, a, y) {if (method=='ce'){return (a-y)}}複製代碼
同時,爲了與咱們的邏輯迴歸例子保持連續,咱們在隱藏層和讀出層上使用 sigmoid 激活函數:
# Calculate activation function
sigmoid <- function(z){1.0/(1.0+exp(-z))}
# Partial derivative of activation function
sigmoid_prime <- function(z){sigmoid(z)*(1-sigmoid(z))}複製代碼
如以前所說,通常來說 softmax 激活函數適用於讀出層。對於隱藏層,線性整流函數(ReLU)更加地廣泛,這裏就是最大值函數(負數被看做爲0)。隱藏層使用的激活函數能夠被想象爲一場扛着火焰同時保持它(梯度)不滅的比賽。sigmoid 函數在0和1處平坦化,成爲一個平坦的梯度,至關於火焰的熄滅(咱們失去了信號)。而線性整流函數(ReLU)幫助保存了這個梯度。
反向傳播函數被定義爲:
backprop <- function(x, y, C, sizes, num_layers, biases, weights)複製代碼
請在筆記中查看完整的代碼-然而原則仍是同樣的:咱們有一個正向傳播,使得咱們在網絡中將權重傳導過全部神經層,併產生預測值。而後將預測值代入損失梯度函數中並將全部神經層中的權重更新。
這總結了神經網絡的建成(搭配上你所須要的儘量多的隱藏層)。將隱藏層的激活函數換爲 ReLU
函數,讀出層換爲 softmax 函數,而且加上 L1 和 L2 的歸一化,是一個不錯的練習。把它在筆記中的 iris 數據集跑一遍,只用一個隱藏層,包含40個神經元,咱們就能夠在大概30多回合訓練後獲得一個96%精確度的神經網絡。
筆記中還提供了一個100個神經元的手寫識別系統的例子,來根據28*28像素的圖像預測數字。
在這裏,咱們只會簡單地測試卷積神經網絡(CNN)中的正向傳播。CNN 首次受到關注是由於1998年的LeCun的精品論文。自此以後,CNN 被證明是在圖像、聲音、視頻甚至文字中最好的算法。
圖像識別開始時是一個手動的過程,研究者們須要明確圖像的哪些比特(特徵)對於識別有用。例如,若是咱們但願將一張圖片歸類進‘貓’或‘籃球’,咱們能夠寫一些代碼提取出顏色(如籃球是棕色)和形狀(貓有着三角形耳朵)。這樣咱們或許就能夠在這些特徵上跑一個線性迴歸,來獲得三角形個數和圖像是貓仍是樹的關係。這個方法很受圖片的大小、角度、質量和光線的影響,有不少問題。規模不變的特徵變換(SIFT) 在此基礎上作了大幅提高並曾被用來對一個物體提供‘特徵描述’,這樣能夠被用來訓練線性迴歸(或其餘的關係型學習器)。然而,這個方法有個一成不變的規則使其不能被爲特定的領域而優化。
CNN 卷積神經網絡用一種頗有趣的方式看待圖像(提取特徵)。開始時,他們只觀察圖像的很小一部分(每次),好比說一個大小爲 5*5 像素的框(一個過濾器)。2D 用於圖像的卷積,是將這個框掃遍整個圖像。這個階段會專門用於提取顏色和線段。然而,下一個神經層會轉而關注以前過濾器的結合,於是‘放大來觀察’。在必定數量的層數以後,神經網絡會放的足夠大而能識別出形狀和更大的結構。
這些過濾器最終會成爲神經網絡須要去學習、識別的‘特徵’。接着,它就能夠經過統計各個特徵的數量來識別其與圖像標籤(如‘籃球’或‘貓’)的關係。這個方法看起來對圖片來說很天然-由於它們能夠被拆成小塊來描述(它們的顏色,紋理等)。CNN 看起來在圖像分形特徵分析方面會蓬勃發展。這也意味着它們不必定適合其餘形式的數據,如 excel 工做單中就沒有固有的樣式:咱們能夠改變任意幾列的順序而數據仍是同樣的——不過在圖像中交換像素點的位置就會致使圖像的改變。
在以前的例子中咱們觀察的是一個標準的神經網絡對手寫字體的歸類。在神經網絡中的 i 層的每一個神經元,與 j 層的每一個神經元相連-咱們所框中的是整個圖像(譯者注:與 CNN 以前的 5*5 像素的框不一樣)。這意味着若是咱們學習了數字 2 的樣子,咱們可能沒法在它被錯誤地顛倒的時候識別出來,由於咱們只見過它正的樣子。CNN 在觀察數字 2 的小的比特時而且在比較樣式的時候有很大的優點。這意味着不少被提取出的特徵對各類旋轉,歪斜等是免疫的(譯者注:即適用於全部變形)。對於更多的細節,Brandon 在這裏解釋了什麼是真正的 CNN。
咱們在 R 中如此定義 2D 卷積函數:
convolution <- function(input_img, filter, show=TRUE, out=FALSE)
{
conv_out <- outer(
1:(nrow(input_img)-kernel_size[[1]]+1),
1:(ncol(input_img)-kernel_size[[2]]+1),
Vectorize(function(r,c) sum(input_img[r:(r+kernel_size[[1]]-1),
c:(c+kernel_size[[2]]-1)]*filter))
)
}複製代碼
並用它對一個圖片應用了一個 3*3 的過濾器:
conv_emboss <- matrix(c(2,0,0,0,-1,0,0,0,-1), nrow = 3)
convolution(input_img = r_img, filter = conv_emboss)複製代碼
你能夠查看筆記來看結果,然而這看起來是從圖片中提取線段。不然,卷積能夠‘銳化’一張圖片,就像一個3*3的過濾器:
conv_sharpen <- matrix(c(0,-1,0,-1,5,-1,0,-1,0), nrow = 3)
convolution(input_img = r_img, filter = conv_sharpen)複製代碼
很顯然咱們能夠隨機地隨機地初始化一些個數的過濾器(如:64個):
filter_map <- lapply(X=c(1:64), FUN=function(x){
# Random matrix of 0, 1, -1
conv_rand <- matrix(sample.int(3, size=9, replace = TRUE), ncol=3)-2
convolution(input_img = r_img, filter = conv_rand, show=FALSE, out=TRUE)
})複製代碼
咱們能夠用如下的函數可視化這個 map:
square_stack_lst_of_matricies <- function(lst)
{
sqr_size <- sqrt(length(lst))
# Stack vertically
cols <- do.call(cbind, lst)
# Split to another dim
dim(cols) <- c(dim(filter_map[[1]])[[1]],
dim(filter_map[[1]])[[1]]*sqr_size,
sqr_size)
# Stack horizontally
do.call(rbind, lapply(1:dim(cols)[3], function(i) cols[, , i]))
}複製代碼
在運行這個函數的時候咱們意識到了整個過程是如何地高密度計算(與標準的全鏈接神經層相比)。若是這些 feature-map 不是那些那麼有用的集合(也就是說,很難在此時下降損失)而後反向傳播會意味着咱們將會獲得不一樣的權重,與不一樣的 feature-map 相關聯,對於進行的聚類頗有幫助。
很明顯的咱們將卷積創建在其餘的卷積中(並且所以須要一個深度網絡)因此線段構成了形狀而形狀構成了鼻子,鼻子構成了臉。測試一些訓練的網絡中的feature map來看看神經網絡實際學到了什麼也是一件有趣的事。
neuralnetworksanddeeplearning.com/
houxianxu.github.io/2015/04/23/…
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。