一般來講,用 PyTorch 處理自然語言比較繁瑣。於是,國外一位開發者Yasufumi TANIGUCHI開發了 LineFlow,爲了儘可能減輕編碼的痛苦,並保證完成同樣的任務。Yasufumi TANIGUCHI表示,LineFlow 要比 PyTorch 簡潔數倍,讓我們來看看 LineFlow 究竟能簡潔到什麼地步?
對自然語言處理任務來說,你可能需要在預處理中對文本進行詞法分析或構建詞彙表。因爲這個過程非常痛苦,所以我創建了LineFlow ,儘可能讓整個過程乾淨整潔。真正的代碼看起來是什麼樣子?請看下面的圖,預處理包括詞法分析、詞彙表構建和索引。
左邊部分是來自 PyTorch 官方示例倉庫的示例代碼,它對文本數據進行常見的預處理。右邊部分是用 LineFolw 編寫的,實現了完全相同的處理。看完對比之後,你應該明白 LineFlow 是如何減輕痛苦的。要查看完整的代碼,可以訪問此鏈接。
在本文中,我將詳細解釋上圖右邊部分的代碼,並講解 LineFlow 的用法。
加載文本數據
文本數據的加載,是通過上面代碼中的第 8 行完成的,我稍後會詳細解釋這個 map。lf.TextDataset
將文本文件的路徑作爲參數並進行加載。
dataset = lf.TextDataset(path, encoding='utf-8').map(...)
lf.TextDataset
要求的數據格式是每行對應一個數據。如果文本數據滿足此條件,則可以加載任何類型的文本數據。
加載之後,它將文本數據轉換爲列表。列表中的項對應於文本數據中的行。 請看下圖,這是 lf.TextDataset
的直觀圖像。圖中的 d
代表代碼中的 dataset
。
LineFlow 已經提供了一些公開可用的數據集。所以你可以馬上使用它。要查看提供的數據集,請訪問此鏈接。
2. 標記化
文本標記化也是通過第 8 行完成的。map
將作爲參數傳遞的處理應用到文本數據的每一行。
dataset = lf.TextDataset(...).map(lambda x: x.split() + ['<eos>'])
請看下圖。這是 lf.TextDataset.map
的直觀圖像。圖中的 d
代表代碼中的 dataset
。
讓我們深入瞭解下面的實際處理過程。
lambda x: x.split() + ['<eos>']
我們將文本數據中的每一行按空格拆分爲標記,然後將 <eos>
添加到這些標記的末尾。我們遵循 WikiText 官方頁面上的處理方式。
此時,我們使用 str.split
進行標記化。我們可以使用其他的標記化方法,如 spaCy、StanfordNLP、Bling Fire 等。例如,如果你想使用 Bling Fire,我們將得到以下代碼。
>>> from blingfire import text_to_words
>>> d = lf.TextDataset('/path/to/your/text')
>>> d.map(text_to_words).map(str.split)
另外,只要我們的處理將每行文本數據作爲參數,就可以執行任何我們想要的處理。例如,我們可以計算標記的數量。在下面的代碼中,標記的數量是在第二個元素中定義的。
>>> d = lf.TextDataset('/path/to/text')
>>> d.map(tokenize).map(lambda x: (x, len(x)))
當我們想要製作用於注意力機制或長短期記憶網絡的掩碼時,這種處理就很有用。
3. 索引
索引是由第 9 行到第 12 行完成的。這些行如下圖所示。在這個代碼塊中,我們構建了詞彙表和索引。讓我們按順序來查看這些內容。
for word in dataset.flat_map(lambda x: x):
self.dictionary.add_word(word)
return torch.LongTensor(dataset.flat_map(...))
首先我們將看到構建詞彙表的代碼塊。在下面的代碼塊中,我們構建了詞彙表。 flat_map
將作爲參數傳遞的處理應用於數據中的每一行,然後對其進行扁平化。因此,我們將在 dataset.flat_map(lambda x: x)
之後獲取單個標記。
for word in dataset.flat_map(lambda x: x):
self.dictionary.add_word(word)
請看下圖。這是 dataset.flat_map(lambda x: x)
的直觀圖像。圖中的 d
代表代碼中的 'dataset`。
flat_map
有點令人困惑,但它等同於下面的代碼。
>>> from itertools import chain
>>> chain.from_iterable(map(lambda x: x, dataset))
>>>
>>> dataset.flat_map(lambda x: x) # same as above
在使用 flat_map
提取每個標記之後,我們將標記傳遞給 self.dictionary.add_word
來構建詞彙表。我將不會解釋它是如何工作的,因爲這與本文無關。但如果你對它的內部實現感興趣的話,請查看此鏈接。
self.dictionary.add_word(word)
接下來,我們將看到索引的代碼塊。索引是由一下的代碼塊來完成的。我們還使用 flat_map
來索引每個標記並使其扁平化。這是因爲 PyTorch 的示例需要扁平化標記的張量,所以我們就這麼做了。
dataset.flat_map(
[lambda x: self.dictionary.word2idx[token] for token in x)])
請看下圖。這是 dataset.flat_map(indexer)
的直觀圖像。圖中的 d
代表代碼中的 dataset
。
此代碼等同於以下代碼。
>>> from itertools import chain
>>> chain.from_iterable(map(indexer, dataset))
>>>
>>> dataset.flat_map(indexer) # same as above
最後,我們用 torch.LongTensor
將它包起來,把它變成張量。至此就完成了文本數據的加載。
return torch.LongTensor(dataset.flat_map(...))
現在我們可以閱讀完整的代碼了,如下所示:
import os
import torch
import lineflow as lf
class Dictionary(object):
def __init__(self):
self.word2idx = {}
self.idx2word = []
def add_word(self, word):
if word not in self.word2idx:
self.idx2word.append(word)
self.word2idx[word] = len(self.idx2word) - 1
return self.word2idx[word]
def __len__(self):
return len(self.idx2word)
class Corpus(object):
def __init__(self, path):
self.dictionary = Dictionary()
self.train = self.tokenize(os.path.join(path, 'train.txt'))
self.valid = self.tokenize(os.path.join(path, 'valid.txt'))
self.test = self.tokenize(os.path.join(path, 'test.txt'))
def tokenize(self, path):
assert os.path.exists(path)
dataset = lf.TextDataset(path, encoding='utf-8').map(lambda x: x.split() + ['<eos>'])
for word in dataset.flat_map(lambda x: x):
self.dictionary.add_word(word)
return torch.LongTensor(dataset.flat_map(
lambda x: [self.dictionary.word2idx[token] for token in x]))
這就是全部的解釋。LineFlow 通過對文本數據進行向量化來完成較少的循環和嵌套代碼。我們可以使用 Python 的 map 來完成同樣的工作。但是,LineFlow 爲我們提供了可讀的、乾淨的代碼,因爲它像管道(Fluent Interface)一樣構建了處理過程。
如果你喜歡 LineFlow,並想了解更多信息,請訪問 LineFlow 在 GitHub 的倉庫。
作者介紹:
Yasufumi TANIGUCHI,軟件工程師,對自然語言處理有着濃厚的興趣。本文最初發表於 Medium 博客,經原作者 Yasufumi TANIGUCHI 授權,InfoQ 中文站翻譯並分享。
原文鏈接:
https://towardsdatascience.com/lineflow-introduction-1caf7851125e