Trong nhiều ví dụ về xử lý dữ liệu lớn, việc đếm từ thường được sử dụng làm ví dụ khởi đầu. Bài viết này cũng không ngoại lệ, lấy ví dụ từ hướng dẫn Table API của PyFlink, chúng ta sẽ học cách đếm số lần xuất hiện của các từ khác nhau thông qua nhiều phương pháp khác nhau để đạt được hiệu quả học tập từng bước. Phương pháp thông thường
# input.py
word_count_data = ["To be, or not to be,--that is the question:--",
"Whether 'tis nobler in the mind to suffer",
"The slings and arrows of outrageous fortune",
"Or to take arms against a sea of troubles,",
"And by opposing end them?--To die,--to sleep,--",
"No more; and by a sleep to say we end",
"The heartache, and the thousand natural shocks",
"That flesh is heir to,--'tis a consummation",
"Devoutly to be wish'd. To die,--to sleep;--",
"To sleep! perchance to dream:--ay, there's the rub;",
"For in that sleep of death what dreams may come,",
"When we have shuffled off this mortal coil,",
"Must give us pause: there's the respect",
"That makes calamity of so long life;",
"For who would bear the whips and scorns of time,",
"The oppressor's wrong, the proud man's contumely,",
"The pangs of despis'd love, the law's delay,",
"The insolence of office, and the spurns",
"That patient merit of the unworthy takes,",
"When he himself might his quietus make",
"With a bare bodkin? who would these fardels bear,",
"To grunt and sweat under a weary life,",
"But that the dread of something after death,--",
"The undiscover'd country, from whose bourn",
"No traveller returns,--puzzles the will,",
"And makes us rather bear those ills we have",
"Than fly to others that we know not of?",
"Thus conscience does make cowards of us all;",
"And thus the native hue of resolution",
"Is sicklied o'er with the pale cast of thought;",
"And enterprises of great pith and moment,",
"With this regard, their currents turn awry,",
"And lose the name of action.--Soft you now!",
"The fair Ophelia!--Nymph, in thy orisons",
"Be all my sins remember'd."]
Cách tiếp cận thông thường của chúng ta là:
- Duyệt qua list này, tách mỗi dòng bằng khoảng trắng thành các từ riêng lẻ, lưu vào một list mới
- Duyệt qua list mới được tạo ra ở bước 1, sử dụng map để ghi lại kết quả thống kê, key là từ, value là số lần xuất hiện
# common.py
from input import word_count_data
wordCount = dict()
for line in word_count_data:
wordsOneline = line.split() # Lấy list các từ sau khi tách dòng
for word in wordsOneline:
wordCount.update({word:wordCount.get(word,0)+1}) # Duyệt qua list các từ đã tách và cập nhật số lượng (nếu có thì tăng 1)
print(wordCount)
{‘To’: 4, ‘be,’: 1, ‘or’: 1, ‘not’: 2, ‘to’: 7, ‘be,–that’: 1, ‘is’: 2, ‘the’: 15, ‘question:–’: 1, ‘Whether’: 1, "'tis": 1, ‘nobler’: 1, ‘in’: 3, ‘mind’: 1, ‘suffer’: 1, ‘The’: 7, ‘slings’: 1, ‘and’: 7, ‘arrows’: 1, ‘of’: 14, ‘outrageous’: 1, ‘fortune’: 1, ‘Or’: 1, ‘take’: 1, ‘arms’: 1, ‘against’: 1, ‘a’: 5, ‘sea’: 1, ‘troubles,’: 1, ‘And’: 5, ‘by’: 2, ‘opposing’: 1, ‘end’: 2, ‘them?–To’: 1, ‘die,–to’: 2, ‘sleep,–’: 1, ‘No’: 2, ‘more;’: 1, ‘sleep’: 2, ‘say’: 1, ‘we’: 4, ‘heartache,’: 1, ‘thousand’: 1, ‘natural’: 1, ‘shocks’: 1, ‘That’: 3, ‘flesh’: 1, ‘heir’: 1, "to,–'tis": 1, ‘consummation’: 1, ‘Devoutly’: 1, ‘be’: 1, "wish’d.": 1, ‘sleep;–’: 1, ‘sleep!’: 1, ‘perchance’: 1, ‘dream:–ay,’: 1, "there’s": 2, ‘rub;’: 1, ‘For’: 2, ‘that’: 3, ‘death’: 1, ‘what’: 1, ‘dreams’: 1, ‘may’: 1, ‘come,’: 1, ‘When’: 2, ‘have’: 2, ‘shuffled’: 1, ‘off’: 1, ‘this’: 2, ‘mortal’: 1, ‘coil,’: 1, ‘Must’: 1, ‘give’: 1, ‘us’: 3, ‘pause:’: 1, ‘respect’: 1, ‘makes’: 2, ‘calamity’: 1, ‘so’: 1, ‘long’: 1, ‘life;’: 1, ‘who’: 2, ‘would’: 2, ‘bear’: 2, ‘whips’: 1, ‘scorns’: 1, ‘time,’: 1, "oppressor’s": 1, ‘wrong,’: 1, ‘proud’: 1, "man’s": 1, ‘contumely,’: 1, ‘pangs’: 1, "despis'd": 1, ‘love,’: 1, "law's": 1, ‘delay,’: 1, ‘insolence’: 1, ‘office,’: 1, ‘spurns’: 1, ‘patient’: 1, ‘merit’: 1, ‘unworthy’: 1, ‘takes,’: 1, ‘he’: 1, ‘himself’: 1, ‘might’: 1, ‘his’: 1, ‘quietus’: 1, ‘make’: 2, ‘With’: 2, ‘bare’: 1, ‘bodkin?’: 1, ‘these’: 1, ‘fardels’: 1, ‘bear,’: 1, ‘grunt’: 1, ‘sweat’: 1, ‘under’: 1, ‘weary’: 1, ‘life,’: 1, ‘But’: 1, ‘dread’: 1, ‘something’: 1, ‘after’: 1, ‘death,–’: 1, "undiscover'd": 1, ‘country,’: 1, ‘from’: 1, ‘whose’: 1, ‘bourn’: 1, ‘traveller’: 1, ‘returns,–puzzles’: 1, ‘will,’: 1, ‘rather’: 1, ‘those’: 1, ‘ills’: 1, ‘Than’: 1, ‘fly’: 1, ‘others’: 1, ‘know’: 1, ‘of?’: 1, ‘Thus’: 1, ‘conscience’: 1, ‘does’: 1, ‘cowards’: 1, ‘all;’: 1, ‘thus’: 1, ‘native’: 1, ‘hue’: 1, ‘resolution’: 1, ‘Is’: 1, ‘sicklied’: 1, "o'er": 1, ‘with’: 1, ‘pale’: 1, ‘cast’: 1, ‘thought;’: 1, ‘enterprises’: 1, ‘great’: 1, ‘pith’: 1, ‘moment,’: 1, ‘regard,’: 1, ‘their’: 1, ‘currents’: 1, ‘turn’: 1, ‘awry,’: 1, ‘lose’: 1, ‘name’: 1, ‘action.–Soft’: 1, ‘you’: 1, ‘now!’: 1, ‘fair’: 1, ‘Ophelia!–Nymph,’: 1, ‘thy’: 1, ‘orisons’: 1, ‘Be’: 1, ‘all’: 1, ‘my’: 1, ‘sins’: 1, "remember'd.": 1}
Đoạn mã trên đã giải quyết vấn đề một cách đơn giản trong một vòng lặp for kép. Nếu không muốn sử dụng vòng lặp for kép, chúng ta cần chuyển nó thành hai vòng lặp for đơn
# common_1.py
from input import word_count_data
words = []
for line in word_count_data:
words.extend(line.split()) # Lưu list các từ đã tách của mỗi dòng vào một list một chiều
wordCount = {}
for word in words:
wordCount.update({word:wordCount.get(word,0)+1}) # Duyệt qua list các từ đã tách và cập nhật số lượng từ (nếu có thì tăng 1)
print(wordCount)
Nếu không muốn sử dụng vòng lặp for một cách rõ ràng, có cách nào khác không? Tại đây chúng ta sẽ giới thiệu map và reduce.
Map
map(func, *iterables) --> map object Tạo một iterator tính toán hàm sử dụng các đối số từ mỗi iterable. Dừng khi iterable ngắn nhất bị cạn kiệt.
Nói một cách đơn giản, map sẽ thực thi phương thức xử lý (tham số thứ hai) trên iterator được truyền vào, và đặt kết quả trả về của phương thức đó vào một cấu trúc, cuối cùng chúng ta có thể sử dụng iterator được trả về bởi map để truy cập từng kết quả tính toán. Ví dụ:
import sys
source=[1,2,3,4,5,6]
iter=map(lambda x: x+1, source)
while True:
try:
print(next(iter))
except StopIteration:
sys.exit()
Kết quả
2 3 4 5 6 7
Trong ví dụ trên, chúng ta đặt hàm xử lý cho map là một hàm ẩn danh, nó sẽ trả về giá trị tăng 1 của mỗi số được duyệt. Áp dụng vào ví dụ đếm từ, chúng ta có thể sử dụng đoạn mã sau, duyệt qua từng dòng của word_count_data, sau đó tách nó bằng khoảng trắng thành list và trả về. Như vậy wordsLists là một iterator của list "mỗi phần tử là một list các từ".
from input import word_count_data
wordsLists=map(lambda line: line.split(), word_count_data) # Tạo list hai chiều, phần tử một chiều là một list, mỗi phần tử trong list này là một từ
[ ['To', 'be,', 'or', 'not', 'to', 'be,–that', 'is', 'the', 'question:–'], ['Whether', "'tis", 'nobler', 'in', 'the', 'mind', 'to', 'suffer'], …… ]
Reduce
functools.reduce(function, iterable[, initializer]) Áp dụng hàm hai đối số một cách tích lũy cho các mục của iterable, từ trái sang phải, để giảm iterable xuống một giá trị duy nhất. Ví dụ, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) tính toán ((((1+2)+3)+4)+5). Đối số trái, x, là giá trị tích lũy và đối số phải, y, là giá trị cập nhật từ iterable. Nếu có tham số khởi tạo tùy chọn, nó được đặt trước các mục của iterable trong phép tính và đóng vai trò là giá trị mặc định khi iterable trống. Nếu không có initializer và iterable chỉ chứa một mục, mục đầu tiên được trả về.
Nó tương đương với đoạn mã sau
def reduce(function, iterable, initializer=None):
it = iter(iterable)
if initializer is None:
value = next(it)
else:
value = initializer
for element in it:
value = function(value, element)
return value
Điểm giống nhau với map là:
- Cả hai đều cần cung cấp một hàm xử lý (tham số đầu tiên)
- Hàm xử lý đều có giá trị trả về
Điểm khác nhau là:
- Hàm xử lý chấp nhận hai tham số
- Chấp nhận tham số thứ ba làm dữ liệu trả về ban đầu
Hãy xem một ví dụ. Trong ví dụ này, tham số y của hàm ẩn danh là một giá trị duyệt nào đó của source; x ban đầu là giá trị khởi tạo 100, sau đó là giá trị trả về của hàm ẩn danh trong lần thực thi trước. Như vậy kết quả dưới đây tương đương với 100+1+2+3+4+5+6.
from functools import reduce
source=[1,2,3,4,5,6]
r=reduce(lambda x,y: x+y, source, 100)
print(r)
121
Áp dụng vào ví dụ đếm từ. Phương thức reduce có thể "giảm" cấu trúc list trong list ở trên thành một list một chiều.
words=reduce(lambda wordsAll,wordsOneline: wordsAll+wordsOneline, wordsLists, [])
Giá trị của words là
['To', 'be,', 'or', 'not', 'to', 'be,–that', 'is', 'the', 'question:–', 'Whether', ……]
Sau đó thực hiện tính toán trên list một chiều này, thống kê số lần xuất hiện của mỗi từ, cũng "giảm thiểu" không gian mà words biểu thị cho các từ.
wordCount=reduce(lambda wordCount,word: wordCount.update({word:wordCount.get(word,0)+1}) or wordCount, words, {})
{‘To’: 4, ‘be,’: 1, ‘or’: 1, ‘not’: 2, ‘to’: 7, ‘be,–that’: 1, ‘is’: 2, ‘the’: 15,……}
Tổng thể来说, map làm cho dữ liệu đầu vào được phân tách (ánh xạ) thành các đơn vị dữ liệu nhỏ nhất; reduce giảm quy mô dữ liệu và cuối cùng tạo ra kết quả. Mã nguồn hoàn chỉnh
# map_reduce.py
from functools import reduce
from input import word_count_data
wordsLists=map(lambda line: line.split(), word_count_data)
words=reduce(lambda wordsAll,wordsOneline: wordsAll+wordsOneline, wordsLists, [])
wordCount=reduce(lambda wordCount,word: wordCount.update({word:wordCount.get(word,0)+1}) or wordCount, words, {})
# wordCount=reduce(lambda wordCount,words: reduce(lambda wordCountInline,word: wordCountInline.update({word:wordCountInline.get(word,0)+1}) or wordCountInline, words, wordCount), wordsLists, {})
print(wordCount)