1. 程式人生 > 其它 >深度學習:如何理解tensorflow文字蘊含的原理

深度學習:如何理解tensorflow文字蘊含的原理

文字的entailment(蘊涵)是一個簡單的邏輯練習,用來判斷一個句子是否可以從另一個句子推斷出來。承擔了文字的entailment任務的計算機程式,試圖將一個有序的句子分類為三個類別中的一種。第一類叫做“positive entailment”,當你用第一個句子來證明第二個句子是正確的時候就會出現。第二個類別,“negative entailment”,是positive entailment的反面。當第一個句子被用來否定第二個句子時,就會出現這種情況。最後,如果這兩個句子沒有關聯,那麼它們就被認為是“neutral entailment”。

作為應用程式的一個組成部分,文字的entailment是有用的。例如,問答系統可以使用文字的entailment來驗證儲存資訊的答案。文字的entailment也可以通過過濾不包含新資訊的句子來增強文件的摘要。其他自然語言處理系統(NLP)也發現類似的應用。

本文將引導你瞭解如何構建一個簡單快捷的神經網路來執行使用TensorFlow.的文字的entailment。

在我們開始之前

除了安裝 TensorFlow version 1.0之外,還要確保安裝:

Jupyter

Numpy

Matplotlib

為了在網路培訓中獲得更好的進步感,歡迎你安裝 TQDM,但這不是必需的。請訪問GitHub上的這篇文章的程式碼和Jupyter筆記本(連結為https://github.com/Steven-Hewitt/Entailment-with-Tensorflow)。我們將使用斯坦福的SNLI資料集來進行我們的訓練,但是我們將使用Jupyter Notebook中的程式碼下載並提取我們需要的資料,所以你不需要手動下載它。如果這是你第一次使用TensorFlow,我建議你看看Aaron Schumacher的文章“Hello, Tensorflow”。

我們將從所有必要的輸入開始,利用Jupyter Notebook在筆記本上顯示圖表和影象。

%matplotlib inline

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import urllib
import sys
import os
import zipfil

文字的entailment示例

在本節中,我們將通過一些文字entailment的例子來說明positive, negative和 neutral entailment。首先,我們來看看positive entailment——例如,當你讀到“Maurita and Jade both were at the scene of the car crash”時,你可以推斷“Multiple people saw the accident”。在這個例句中,我們可以用第一個句子(也稱為“文字”)證明第二句(也稱為“假設”),這代表是positive entailment。鑑於莫麗塔和傑德都看到車禍,說明有多人看到。注意:“車禍”和“事故”有相似的意思,但它們不是同一個詞。事實上,entailment並不總是意味著句子裡有相同的單詞,就像在這句話中可以看到的相同的詞只有“the”。

讓我們考慮另一個句子對。“在公園裡和老人一起玩的兩隻狗”推匯出“那天公園裡只有一隻狗”。第一句話說有“兩隻狗”,那麼公園至少要有兩隻狗,第二句話與這個觀點相矛盾,所以是negative entailment。

最後,為了闡明neutral entailment,我們看“我和孩子們打棒球”和“孩子們愛吃冰淇淋”這兩句話,打棒球和愛吃冰淇淋完全沒有任何關係。我可以和冰淇淋愛好者打棒球,我也可以和不喜歡冰淇淋的人打棒球(兩者都是可能的)。因此,第一句話沒有說明第二句話的真實或虛假。

數字向量化表示單詞

對於神經網路來說,它們主要是處理數值。為了解決這個問題,我們需要用數字來表示我們的單詞。理想情況下,這些數字意味著什麼,例如,我們可以使用字母的字元編碼一個詞,但這並沒有告訴我們任何關於它的意義(這意味著TensorFlow不得不做大量的工作來說明“dog”和“canine”是接近相同的概念)。將類似的意義轉化為神經網路可以理解的過程,這個過程被稱為向量化。

建立 word vectorization的常用方法是讓每個單詞表示一個非常高維空間中的一個點。具有相似表示法的單詞應該在這個空間中相對接近。例如,每種顏色都有一個通常與其他顏色非常相似的表示;這一點的演示在關於 word vectorization的TensorFlow教程中可以找到(連結地址是https://www.tensorflow.org/tutorials/word2vec)。

使用斯坦福的GloVe word vectorization+ SNLI資料集

為了我們的目的,我們不需要建立一個新的用數字表現形式。如果通用資料不夠用,可以用已經存在的一些非常出色的通用矢量表示,以及用於培訓更專業的材料的方法。

這篇文章的相關筆記本(連結地址為https://github.com/Steven-Hewitt/Entailment-with-Tensorflow)是為斯坦福的GloVe word vectorization的預先訓練的資料(連結地址為http://nlp.stanford.edu/projects/glove/)而設計的。我們將使用60億的Wikipedia 2014 + Gigaword 5向量,因為它是最小並且最容易下載的。我們將以程式設計方式下載該檔案,執行它可能需要一段時間(這是一個相當大的檔案)。

與此同時,我們收集我們的textual entailment資料集:斯坦福大學SNLI資料集。

glove_zip_file= "glove.6B.zip"
glove_vectors_file= "glove.6B.50d.txt"

snli_zip_file= "snli_1.0.zip"
snli_dev_file= "snli_1.0_dev.txt"
snli_full_dataset_file= "snli_1.0_train.txt"
from six.moves.url.lib.requestimport urlretrieve

#large file - 862 MB
if (not os.path.isfile(glove_zip_file)and
    not os.path.isfile(glove_vectors_file)):
    urlretrieve ("http://nlp.stanford.edu/data/glove.6B.zip",
                 glove_zip_file)

#medium-sized file - 94.6 MB
if (not os.path.isfile(snli_zip_file)and
    not os.path.isfile(snli_dev_file)):
    urlretrieve ("https://nlp.stanford.edu/projects/snli/snli_1.0.zip",
                 snli_zip_file)
def unzip_single_file(zip_file_name, output_file_name):
    """
        If the outFile is already created, don't recreate
        If the outFile does not exist, create it from the zipFile
    """
    if not os.path.isfile(output_file_name):
        withopen(output_file_name,'wb') as out_file:
            with zipfile.ZipFile(zip_file_name) as zipped:
                for infoin zipped.infolist():
                    if output_file_namein info.filename:
                        with zipped.open(info) as requested_file:
                            out_file.write(requested_file.read())
                            return

unzip_single_file(glove_zip_file, glove_vectors_file)
unzip_single_file(snli_zip_file, snli_dev_file)
# unzip_single_file(snli_zip_file, snli_full_dataset_file)

現在我們已經下載了GloVe 向量,我們可以將它們載入到記憶體中,將空間分隔的格式序列化為Python字典:

glove_wordmap= {}
withopen(glove_vectors_file,"r") as glove:
    for linein glove:
        name, vector= tuple(line.split(" ",1))
        glove_wordmap[name]= np.fromstring(vector, sep=" ")

一旦我們有了words,就需要我們的輸入來包含整個句子,並通過一個神經網路來處理它。從製作這個序列開始:

def sentence2sequence(sentence):
    """

    - Turns an input sentence into an (n,d) matrix,
        where n is the number of tokens in the sentence
        and d is the number of dimensions each word vector has.

      Tensorflow doesn't need to be used here, as simply
      turning the sentence into a sequence based off our
      mapping does not need the computational power that
      Tensorflow provides. Normal Python suffices for this task.
    """
    tokens= sentence.lower().split(" ")
    rows= []
    words= []
    #Greedy search for tokens
    for tokenin tokens:
        i= len(token)
        while len(token) >0 and i >0:
            word= token[:i]
            if wordin glove_wordmap:
                rows.append(glove_wordmap[word])
                words.append(word)
                token= token[i:]
                i= len(token)
            else:
                i= i-1
    return rows, words

當計算機研究句子的時會看到什麼

為了更好地理解word vectorization過程,以及看到計算機研究句子時所看到的東西,我們可以將這些向量表示為影象。使用notebook ,視覺化自己的句子。每一行表示一個詞,而列表示向量化字的個體維度。向量化是在根據與其他單詞的關係進行訓練的,實際上表示的含義是含糊不清的。計算機能理解這種向量語言,這是最重要的部分。一般來說,在相同的位置上包含相似顏色的兩個向量表示單詞在意義上相似。

def visualize(sentence):
    rows, words= sentence2sequence(sentence)
    mat= np.vstack(rows)

    fig= plt.figure()
    ax= fig.add_subplot(111)
    shown= ax.matshow(mat, aspect="auto")
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
    fig.colorbar(shown)

    ax.set_yticklabels([""]+words)
    plt.show()

visualize("The quick brown fox jumped over the lazy dog.")
visualize("The pretty flowers shone in the sunlight.")

與影象不同的是,句子有固有的順序,不受大小的約束,所以我們需要一種新的網路型別,而不是完全連線前饋網路,因為前饋網路佔據一個輸入值並且只需執行到產生一個輸出。而我們需要迴圈。

Vanilla迴圈網路

Recurrent neural networks(RNNs)是神經網路的一種序列學習工具。這種型別的神經網路只有一層的隱藏輸入,它被重新用於序列中的每個輸入,以及傳遞給下一個輸入計算的“memory”。這些是用矩陣乘法計算的,其中矩陣索引是訓練的權重,就像它們在完全連線的層中一樣。

對序列中的每一個輸入重複同樣的計算,這意味著一個Recurrent neural networks的一個“層”可以被展開成許多層。事實上,將有與序列中的輸入一樣多的層。這使得網路能夠處理一個非常複雜的句子。TensorFlow包含了它自己的一個簡單RNN cell,BasicRNNCell的實現,它可以新增到你的TensorFlow中,如下圖:

rnn_size= 64
rnn= tf.contrib.rnn.BasicRNNCell(rnn_size)

梯度消失問題

從理論上講,網路將能夠記住來自第一層的東西,更早的在句子中,甚至在句子的末尾。這種迴圈形式的主要問題是:在實踐中,早期的資料完全被更新的輸入和資訊完全淹沒,而這些輸入和資訊並不是那麼重要。Recurrent neural networks,或者是帶有標準隱藏單位的神經網路,在很長一段時間內都不能保持資訊。這個故障被稱為梯度消失問題。

最簡單的方法就是通過示例視覺化。在最簡單的情況下,輸入和“memory”大致相同。資料的第一個輸入將影響第一個輸出的大約一半(另一半是啟動“memory”),第二次輸出的四分之一,然後是第三輸出的八分之一,等等。

這意味著我們不能使用vanilla迴圈網路,如果我們想要對這兩個句子進行追蹤。解決方案是使用不同型別的迴圈網路層。也許最簡單的就是長短期記憶層,也就是LSTM。

利用LSTM

在LSTM中,代替計算當前儲存器時每次都使用相同方式的輸入(xt),網路可以通過“輸入門”(it)決定當前值對儲存器的影響程度做出一個決定,通過被命名為“忘記門”(ft)遺忘的儲存器(ct)做出另外一個決定,根據儲存器將某些部分通過“輸出門”(ot)傳送到下一個時間步長(ht)做第三個決定。

這三個門的組合創造了一個選擇:一個單一的LSTM節點,可以將資訊儲存在長期儲存器中,也可以將資訊儲存在短期儲存器中,但同時不能同時進行。短期記憶LSTMs訓練的是相對開放的輸入門,讓大量的資訊進來,也經常忘記很多,而長期記憶LSTMs有緊密的輸入門,只允許非常小的,非常具體的資訊進入。這種緊密性意味著它不會輕易失去它的資訊,允許更長的儲存時間。

總的來說,LSTMs是非常神祕的。不同LSTM節點在同一個網路可能會有截然不同的門,彼此依賴,比如可能有短期門記住“not”這個詞在句子“ohn did not go to the store”,當“go”這個詞出現,長期門能記住“not go”,而不是“go”。當然,這是一個人為的例子,在實踐中,這些關係非常複雜,以至於無法辨認。

為我們的網路定義常量

由於我們不打算在我們的網路中使用一個普通RNN層,所以我們會清除圖表並新增一個LSTM層,預設情況下也包含TensorFlow。因為這將是我們實際網路的第一部分,定義我們需要的網路的所有常數:

#Constants setup
max_hypothesis_length, max_evidence_length= 30,30
batch_size, vector_size, hidden_size= 128,50,64

lstm_size= hidden_size

weight_decay= 0.0001

learning_rate= 1

input_p, output_p= 0.5,0.5

training_iterations_count= 100000

display_step= 10

def score_setup(row):
    convert_dict= {
      'entailment':0,
      'neutral':1,
      'contradiction':2
    }
    score= np.zeros((3,))
    for xin range(1,6):
        tag= row["label"+str(x)]
        if tagin convert_dict: score[convert_dict[tag]]+= 1
    return score/ (1.0*np.sum(score))

def fit_to_size(matrix, shape):
    res= np.zeros(shape)
    slices= [slice(0,min(dim,shape[e]))for e, dimin enumerate(matrix.shape)]
    res[slices]= matrix[slices]
    return res
def split_data_into_scores():
    import csv
    withopen("snli_1.0_dev.txt","r") as data:
        train= csv.DictReader(data, delimiter='t')
        evi_sentences= []
        hyp_sentences= []
        labels= []
        scores= []
        for rowin train:
            hyp_sentences.append(np.vstack(
                    sentence2sequence(row["sentence1"].lower())[0]))
            evi_sentences.append(np.vstack(
                    sentence2sequence(row["sentence2"].lower())[0]))
            labels.append(row["gold_label"])
            scores.append(score_setup(row))

        hyp_sentences= np.stack([fit_to_size(x, (max_hypothesis_length, vector_size))
                          for xin hyp_sentences])
        evi_sentences= np.stack([fit_to_size(x, (max_evidence_length, vector_size))
                          for xin evi_sentences])

        return (hyp_sentences, evi_sentences), labels, np.array(scores)

data_feature_list, correct_values, correct_scores= split_data_into_scores()

l_h, l_e= max_hypothesis_length, max_evidence_length
N, D, H= batch_size, vector_size, hidden_size
l_seq= l_h+ l_e

我們還將重新設定圖表,不包括我們之前新增的RNN單元,因為我們不會在這個網路中使用它:

tf.reset_default_graph()

有了這兩種方法,我們就可以使用TensorFlow定義我們的LSTM,它的使用方式如下:

lstm= tf.contrib.rnn.BasicLSTMCell(lstm_size)

實現dropout正則化

如果我們只是簡單地使用了LSTM層,而沒有更多的東西,那麼這個網路可能會讀到很多關於普通的,但無關緊要的詞,比如“a”、“the”、和“and”。如果一個句子使用“an animal”這個短語,而另一個句子使用“the animal”,即使這些短語指的是同一個物件,網路也可能錯誤地認為它已經找到了negative entailment。

為了解決這個問題,我們需要調整一下,看看個別單詞最終是否對整體有重要意義,我們通過一個叫“dropout”的過程來實現。dropout是神經網路設計中的一種正則化模式,它圍繞著隨機選擇的隱藏和可見的單位。隨著神經網路的大小增加,用來計算最終結果的引數個數也隨著增加,如果一次訓練全部,每個引數都有助於過度擬合。為了規範這一點,在訓練中隨機抽取網路中包含的部分,並在訓練時臨時調零,在實際使用過程中,它們的輸出被適當地縮放。

“標準”(即完全連線)層上的dropout也是有用的,因為它有效地訓練了多個較小的網路,然後在測試時間內組合它們。機器學習中的一個常數使自己比單個模型更好的方法就是組合多個模型,並且 dropout 用於將單個神經網路轉換為共享一些節點的多個較小的神經網路。

一個dropout 層有一個稱為p的超引數,它僅僅是每個單元被儲存在網路中進行迭代訓練的概率。被儲存的單位將其輸出提供給下一層,而不被儲存的單位則沒有提供任何東西。下面是一個例子,展示了一個沒有dropout的完全連線的網路和一個在迭代訓練過程中dropout的完全連線的網路之間的區別:

用於復發層的Tensorflow的DropoutWrapper

dropout 在LSTM層的內部門上並沒有特別好。某些關鍵記憶的丟失意味著,一階邏輯所需的複雜關係很難與一個dropout形成,所以對於我們的LSTM層,我們將跳過在內部的門上的dropou,而不是在其他的東西上使用它。值得慶幸的是,這是Tensorflow的 DropoutWrapper對於迴圈層的預設實現。

lstm_drop=  tf.contrib.rnn.DropoutWrapper(lstm, input_p, output_p)

完成我們的模型

有了所有的解釋,我們可以完成我們的模型。第一步是標記化,用我們的GloVe字典把兩個輸入的句子變成一個向量序列。由於我們不能有效地使用在LSTM中傳遞的資訊,我們將使用從單詞和最終輸出的功能上的dropout,而不是在展開的LSTM網路部分的第一層和最後一層有效地使用dropout。

您可能注意到我們使用了一個雙向RNN,它有兩個不同的LSTM單元。這種形式的迴圈網路既向前又向後地執行輸入資料,這使得網路既能夠獨立又能相互之間對假設和證據進行審查。

LSTMs最終的輸出將被傳遞到一套完整的連線層,然後,我們將得到一個實值的分數表明每entailment的強度,我們用來選擇最終結果,並確定我們的信心,結果。

# N: The number of elements in each of our batches,
#   which we use to train subsets of data for efficiency's sake.
# l_h: The maximum length of a hypothesis, or the second sentence.  This is
#   used because training an RNN is extraordinarily difficult without
#   rolling it out to a fixed length.
# l_e: The maximum length of evidence, the first sentence.  This is used
#   because training an RNN is extraordinarily difficult without
#   rolling it out to a fixed length.
# D: The size of our used GloVe or other vectors.
hyp= tf.placeholder(tf.float32, [N, l_h, D],'hypothesis')
evi= tf.placeholder(tf.float32, [N, l_e, D],'evidence')
y= tf.placeholder(tf.float32, [N,3],'label')
# hyp: Where the hypotheses will be stored during training.
# evi: Where the evidences will be stored during training.
# y: Where correct scores will be stored during training.

# lstm_size: the size of the gates in the LSTM,
#    as in the first LSTM layer's initialization.
lstm_back= tf.contrib.rnn.BasicLSTMCell(lstm_size)
# lstm_back:  The LSTM used for looking backwards
#   through the sentences, similar to lstm.

# input_p: the probability that inputs to the LSTM will be retained at each
#   iteration of dropout.
# output_p: the probability that outputs from the LSTM will be retained at
#   each iteration of dropout.
lstm_drop_back= tf.contrib.rnn.DropoutWrapper(lstm_back, input_p, output_p)
# lstm_drop_back:  A dropout wrapper for lstm_back, like lstm_drop.


fc_initializer= tf.random_normal_initializer(stddev=0.1)
# fc_initializer: initial values for the fully connected layer's weights.
# hidden_size: the size of the outputs from each lstm layer. 
#   Multiplied by 2 to account for the two LSTMs.
fc_weight= tf.get_variable('fc_weight', [2*hidden_size,3],
                            initializer= fc_initializer)
# fc_weight: Storage for the fully connected layer's weights.
fc_bias= tf.get_variable('bias', [3])
# fc_bias: Storage for the fully connected layer's bias.

# tf.GraphKeys.REGULARIZATION_LOSSES:  A key to a collection in the graph
#   designated for losses due to regularization.
#   In this case, this portion of loss is regularization on the weights
#   for the fully connected layer.
tf.add_to_collection(tf.GraphKeys.REGULARIZATION_LOSSES,
                     tf.nn.l2_loss(fc_weight))

x= tf.concat([hyp, evi],1)# N, (Lh+Le), d
# Permuting batch_size and n_steps
x= tf.transpose(x, [1,0,2])# (Le+Lh), N, d
# Reshaping to (n_steps*batch_size, n_input)
x= tf.reshape(x, [-1, vector_size])# (Le+Lh)*N, d
# Split to get a list of 'n_steps' tensors of shape (batch_size, n_input)
x= tf.split(x, l_seq,)

# x: the inputs to the bidirectional_rnn


# tf.contrib.rnn.static_bidirectional_rnn: Runs the input through
#   two recurrent networks, one that runs the inputs forward and one
#   that runs the inputs in reversed order, combining the outputs.
rnn_outputs, _, _= tf.contrib.rnn.static_bidirectional_rnn(lstm, lstm_back,
                                                            x, dtype=tf.float32)
# rnn_outputs: the list of LSTM outputs, as a list.
#   What we want is the latest output, rnn_outputs[-1]

classification_scores= tf.matmul(rnn_outputs[-1], fc_weight)+ fc_bias
# The scores are relative certainties for how likely the output matches
#   a certain entailment:
#     0: Positive entailment
#     1: Neutral entailment
#     2: Negative entailment

展示TensorFlow如何計算準確度

為了測試精度並開始增加優化約束,我們需要展示TensorFlow如何計算準確預測標籤的精度或百分比。

我們還需要確定一個損失,以顯示網路的執行狀況。由於我們有分類分數和最優分數,所以這裡的選擇是使用來自TensorFlow的softmax損失的變化:tf.nn.softmax_cross_entropy_with_logits。我們增加了正則化的損失以幫助過度擬合,然後準備一個優化器來學習如何減少損失。

with tf.variable_scope('Accuracy'):
    predicts= tf.cast(tf.argmax(classification_scores,1),'int32')
    y_label= tf.cast(tf.argmax(y,1),'int32')
    corrects= tf.equal(predicts, y_label)
    num_corrects= tf.reduce_sum(tf.cast(corrects, tf.float32))
    accuracy= tf.reduce_mean(tf.cast(corrects, tf.float32))

with tf.variable_scope("loss"):
    cross_entropy= tf.nn.softmax_cross_entropy_with_logits(
        logits= classification_scores, labels= y)
    loss= tf.reduce_mean(cro   ss_entropy)
    total_loss= loss+ weight_decay* tf.add_n(
        tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))

optimizer= tf.train.GradientDescentOptimizer(learning_rate)

opt_op= optimizer.minimize(total_loss)

訓練網路

最後,訓練網路。如果你安裝了TQDM,可以使用它來跟蹤網路訓練的進度。

# Initialize variables
init= tf.global_variables_initializer()

# Use TQDM if installed
tqdm_installed= False
try:
    from tqdmimport tqdm
    tqdm_installed= True
except:
    pass

# Launch the Tensorflow session
sess= tf.Session()
sess.run(init)

# training_iterations_count: The number of data pieces to train on in total
# batch_size: The number of data pieces per batch
training_iterations= range(0,training_iterations_count,batch_size)
if tqdm_installed:
    # Add a progress bar if TQDM is installed
    training_iterations= tqdm(training_iterations)

for iin training_iterations:

    # Select indices for a random data subset
    batch= np.random.randint(data_feature_list[0].shape[0], size=batch_size)

    # Use the selected subset indices to initialize the graph's
    #   placeholder values
    hyps, evis, ys= (data_feature_list[0][batch,:],
                      data_feature_list[1][batch,:],
                      correct_scores[batch])

    # Run the optimization with these initialized values
    sess.run([opt_op], feed_dict={hyp: hyps, evi: evis, y: ys})
    # display_step: how often the accuracy and loss should
    #   be tested and displayed.
    if (i/batch_size)% display_step== 0:
        # Calculate batch accuracy
        acc= sess.run(accuracy, feed_dict={hyp: hyps, evi: evis, y: ys})
        # Calculate batch loss
        tmp_loss= sess.run(loss, feed_dict={hyp: hyps, evi: evis, y: ys})
        # Display results
        print("Iter " + str(i/batch_size)+ ", Minibatch Loss= " + 
              "{:.6f}".format(tmp_loss)+ ", Training Accuracy= " + 
              "{:.5f}".format(acc))

網路現在被訓練。應該看到大約50 – 55%的準確性,可以通過仔細修改超引數和增加資料集的大小以包括整個訓練集來改進。通常,這將與訓練時間的增加相對應。

通過插入自己的句子,修改在notebook上程式碼:

evidences= ["Maurita and Jade both were at the scene of the car crash."]

hypotheses= ["Multiple people saw the accident."]

sentence1= [fit_to_size(np.vstack(sentence2sequence(evidence)[0]),
                         (30,50))for evidencein evidences]

sentence2= [fit_to_size(np.vstack(sentence2sequence(hypothesis)[0]),
                         (30,50))for hypothesisin hypotheses]

prediction= sess.run(classification_scores, feed_dict={hyp: (sentence1* N),
                                                        evi: (sentence2* N),
                                                        y: [[0,0,0]]*N})
print(["Positive","Neutral","Negative"][np.argmax(prediction[0])]+
      " entailment")

最後,我們完成了模型的操作,結束會話以釋放系統資源。

sess.close()

本文為編譯作品,作者 Steven Hewitt,原網址為

https://www.oreilly.com/learning/textual-entailment-with-tensorflow