Tag: python

  • Tạo Telegram bot để phục vụ làm việc với GoogleSheet

    Tạo Telegram bot để phục vụ làm việc với GoogleSheet

    1. Bài toán

    • Việc truy xuất các dữ liệu trên một file Excel hay GoogleSheet là rất dễ dàng với những người có kĩ năng Office cơ bản.
    • Tuy nhiên với một người không có kiến thức đủ để làm việc với Excel, GoogleSheet hoặc muốn truy xuất thông tin từ file ngay lập tức mà lại đang không có các công cụ hỗ trợ làm việc, viết các câu lệnh trên Excel, GoogleSheet, thì việc truy xuất dữ liệu này sẽ trở nên khó khăn.
    • Khi đó việc tạo một trợ lý ảo, một con bot giúp người dùng có thể truy cập đến dữ liệu trên một file Excel, GoogleSheet là một việc rất hữu ích.
    • Ở đây tôi sẽ dùng bot trên Telegram vì đây là một ứng dụng nhắn tin khá phổ biến và cho phép developer phát triển nhiều thứ trên đó.
    • Về ngôn ngữ lập trình tôi sẽ sử dụng Python vì ngôn ngữ này cho phép developer làm việc dễ dàng với Excel, GoogleSheet.

    2. Tạo bot trên Telegram

    • Đầu tiên người dùng cần phải truy cập vào ứng dụng Telegram, vào thanh tìm kiếm và tìm kiếm từ khoá BotFather
    • Gõ lệnh /newbot để tạo bắt đầu tạo bot mới
    • Tiếp theo chúng ta sẽ đặt tên cho bot của mình.
    • Sau khi đặt tên BotFather sẽ yêu cầu chúng ta chọn username cho con bot, đây cũng chính là tên giúp chúng ta có thể tìm kiếm con bot ở trên Telegram như các tìm kiếm một người dùng.
    Sau khi hoàn thành xong bước trên BotFather sẽ gửi lại cho chúng ta thông tin về bot
    • Link để truy cập vào bot: t.me/csv_xlsx_bot
    • Access token: developer sẽ sử dụng token để làm việc với các API từ đó phát triển các tính năng khác cho con bot của mình.

    3. Sử dụng Python để truy xuất data từ GoogleSheet

    • Đầu tiên chúng ta cần tạo một project Python
    • Thêm các thư viện cần thiết: pip, pandas, numpy
    • Chạy các câu lệnh sau ở terminal để thêm các thư viện cần dùng
    • Lưu ý là link GoogleSheet cần được public access cho tất cả người dùng có thể truy cập
    # Install pip
    python3 get-pip.py
    # Install pandas
    pip install pandas
    # Install numpy
    pip install numpy

    Tạo file .env: file này được dùng để lưu token của Telegram API developer

    export BOT_TOKEN=6354775494:*************************************

    Tạo một file đặt tên là utils.py: file này sẽ chứa các thuật toán làm việc với file GoogleSheet

    • Chúng ta sẽ sử dụng thư viện pandas đọc file GoogleSheet
    • Các trường thông tin truy vấn được sử dụng ở đây là
    Các trường thông tin truy vấn được sử dụng:
    • download_link: đường dẫn file GoogleSheet cần truy vấn
    • sheet_name: tab trong file GoogleSheet cần truy vấn
    • usecols: các cột giá trị truy vấn
    • df = df[(df[‘City’]==’Los Angeles’)]: dùng để filter các bản ghi có trường ‘City’ là ‘Los Angeles’
    Kết quả trả về sẽ được parse sang string bằng việc sử dụng thư viện numpy
    import requests
    import pandas as pd
    import numpy as np
    
    download_link_prefix = "https://drive.google.com/uc?export=download&id="
    def read_data_from_file(file_path):
        sheet_name = 'FoodSales'
        query_cols = ['ID','Date','City']
        download_link = get_google_sheets_download_link(file_path)
        if (download_link == None): return None
        else: 
            df = pd.read_excel(download_link, sheet_name=sheet_name, usecols=query_cols)
            df = df[(df['City']=='Los Angeles')]
            mat = np.array(df)
            return np.array2string(mat)
    
    def get_google_sheets_download_link(file_path):
        file_split= file_path.split('/')
        if len(file_split) > 5:
            return download_link_prefix + file_split[5]
        else:
            return None

    Tạo file main.py: file này sẽ bao gồm các thao tác làm việc với bot Telegram

    • bot = telebot.TeleBot(BOT_TOKEN): tạo một instance của bot Telegram
    • @bot.message_handler(commands=[]): đoạn code sẽ tạo phương thức lắng nghe lệnh điều khiển từ người dùng
    • bot.reply_to(): gửi lại tin nhắn tới người dùng
    • bot.infinity_polling(): câu lệnh cho phép bot Telegram sẽ lắng nghe liên tục các thông tin, lệnh từ phía người dùng
    • bot.send_message(): gửi lại câu trả lời cho tin nhắn nhận được từ người dùng
    • bot.register_next_step_handler(): đăng ký function sẽ được thực thi ngay sau khi nhận được phản hồi từ phía người dùng
    • file_path = message.text: đọc tin nhắn, câu lệnh được gửi từ phía người dùng
    import os
    import telebot
    from dotenv import load_dotenv
    load_dotenv()
    from utils import read_data_from_file, write_data_to_file
    
    BOT_TOKEN = os.environ.get('BOT_TOKEN')
    
    bot = telebot.TeleBot(BOT_TOKEN)
    
    @bot.message_handler(commands=['start', 'hello'])
    def send_welcome(message):
        bot.reply_to(message, "Howdy, how are you doing?")
    
    @bot.message_handler(commands=['read_record'])
    def send_welcome(message):
        sent_msg = bot.send_message(message.chat.id, "So, what link would you like to read?")
        bot.register_next_step_handler(sent_msg, read_csv_xlsx_handler)
    
    def read_csv_xlsx_handler(message):
        file_path = message.text
        result = read_data_from_file(file_path)
        if (result == None): bot.send_message(message.chat.id, "Could not read")
        else: bot.send_message(message.chat.id, result)
    
    bot.infinity_polling()
    • Để chạy con bot của mình ta cần gõ lệnh sau trên terminal:
    python3 main.py

    4. Kết quả

    • File được dùng để đọc dữ liệu: Sample Link
    • Video Demo

    5. Tham khảo

    • Telegram Bot Developer API: https://core.telegram.org/bots/features
    • Pandas: https://pandas.pydata.org/docs/
    • IDE: Visual Studio Code
    • Language: Python
  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 4

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 4

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 4

    Ở bài viết này, tôi sẽ giới thiệu một kỹ thuật gọi là decorator. Nhìn chung, nếu ai đã từng làm việc với các python web framework như Django, Flask, FastAPI,… thì sẽ sử dụng kỹ thuật này thường xuyên, nhưng không phải ai cũng hiểu rõ bản chất của nó.

    Thừa nhận rằng, khi sử dụng các decorators có sẵn mà các framework cung cấp, đã đủ để chúng ta làm việc như một web developer. Tuy nhiên, việc nắm được bản chất của kỹ thuật này cũng giúp cho chúng ta sử dụng decorators hiệu quả hơn, có thể tùy chỉnh và tạo ra các decorators của riêng mình, như vậy là ta đã trở thành một lập trình viên chuyên nghiệp hơn.

    Table of contents

    Decorators

    Nhắc lại một ví dụ trong Phần 3, chúng ta đã áp dụng Closure để maintain một bộ đếm – đếm số lần gọi một hàm bất kỳ.

    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('Hàm {0} đã được gọi {1} lần'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
    
        return inner

    Hàm counter nhận một function làm đầu vào – fn. Trong hàm counter, ta khởi tạo 1 biến cục bộ cnt – biến này sẽ đến số lần gọi hàm fn, mỗi khi goị đến hàm inner thì biến cnt được tăng lên 1 đơn vị.

    Việc sử dụng *args**kwargs trong hàm inner giúp ta có thể gọi hàm fn với bất kỳ sự kết hợp positional argumentskeyword-only arguments nào. Lấy ví dụ:

    def mult(x, y=10):
        """
        return the products of two values
        """
        return x * y
    
    mult = counter(mult) # trả về inner function - closure

    Nhớ rằng, ban đầu mult là một label trỏ đến hàm mult được định nghĩa ở trên. Goị hàm counter(mult) sẽ trả về một closure và gán vào một label là mult, thì lúc này mult sẽ trỏ đến closure đó, rõ ràng là khác so với nơi mà mult ban đầu trỏ đến.

    Tuy nhiên, inner thực hiện gọi hàm mult ban đầu cho chúng ta và trả về kết quả của nó, chính vì vậy mà kết quả trả về khi gọi hàm inner không khác so với việc gọi hàm mult ban đầu. Nhưng thực sự, hàm inner đã làm thêm một số việc trước khi gọi và trả về kết quả của hàm mult, đó là đếm số lần hàm mult được gọi.

    mult(3, 5)
    # In ra: Hàm mult đã được gọi 1 lần
    # return 15

    Chúng ta đã thực sự sửa đổi hàm mult ban đầu, bằng cách wrapping nó bên trong một hàm khác – bổ sung thêm chức năng cho nó –> chúng ta đã decorated hàm mult với hàm counter và chúng ta gọi counterdecorator function.

    Nhìn chung, một decorator function sẽ:

    • lấy 1 function như một đối số
    • return một closure
    • closure nói trên sẽ nhận bất kỳ sự kết hợp đầu vào nào (sử dụng *args và **kwargs)
    • chạy một vài chức năng bên trong closure
    • closure function sẽ gọi original function sử dụng các đối số được truyền vào closure
    • return kết quả được trả về từ function call nói trên

    The @ Symbol

    Như đã thấy, chúng ta hoàn toàn có thể sử dụng decorator function như sau:

    def my_func(*args, **kwargs):
        # some code here
        # ...
    my_func = func(my_func)

    Trong đó, funcdecorator function, còn my_func là hàm được decorated.

    Tuy nhiên, có một cách khác thanh lịch hơn:

    @func
    def my_func(*args, **kwargs):
        # some code here
        # ...

    Introspection

    Sử dụng lại counter như một decorator cho hàm mult:

    import inspect
    
    @counter
    def mult(x, y=10):
        """
        return the products of two values
        """
        return x * y
    
    print(f'name = {mult.__name__}') # name = inner

    Ta thấy rằng, câu lệnh print ở trên sẽ in ra màn hình name = inner. Rõ ràng, tên của hàm mult không phải là mult nữa, mà nó là inner – tên của một closure, điều này chứng tỏ một điều rằng, hàm mult lúc này chính là một closure.

    Vì vậy, cầu lưu ý rằng, khi chúng ta decorate một function, tức là chúng ta đã làm cho function đó thay đổi (về bản chất).

    Để chắc chắn hơn, hãy thử gọi:

    print(f'signature: {inspect.signature(mult)}') # signature: (*args, **kwargs)

    Như vậy, khi gọi inspect.signature(mult), chúng ta đã nhìn thấy rõ chữ ký của hàm inner được trả về.

    Việc decorate một function làm thay đổi docstring, signature, name khiến cho việc debugging trở nên khó khăn hơn rất nhiều.

    The wraps function

    Để giải quyết vấn đề mới nói ở trên, functools module cung cấp một wraps function có thể fix metadata của inner function trong decorator. Bản thân wraps function này là một decorator.

    Hàm wraps này sẽ decorate inner function để thay đổi metadata (docstring, signature, name,…) của nó. Thêm nữa, nó phải sử dụng hàm mult như một đối số đầu vào, để có thể thay thế metadata của inner function bằng metadata của mult function (hàm được decorated)

    from functools import wraps
    import inspect
    
    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        @wraps(fn)
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('Hàm {0} đã được gọi {1} lần'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
        return inner
    
    @counter
    def mult(x, y=10):
        """
        return the products of two values
        """
        return x * y
    
    print(f'name = {mult.__name__}') # name = mult
    print(f'signature: {inspect.signature(mult)}') # signature: (x, y=10)

    Chúng ta không nhất thiết phải sử dụng wraps function trong khi sử dụng decorator, nhưng việc sử dụng nó sẽ giúp cho việc debugging trở nên dễ dàng hơn.

    Summary

    1. Decorators trong Python là cú pháp cho phép một function sửa đổi một function khác tại thời gian chạy (runtime)
    2. Ký pháp @ giúp cho việc sử dụng decorator thanh lịch hơn (mặc dù có thể không cần)
    3. Hãy sử dụng wraps function giúp cho việc debugging dễ dàng hơn khi làm việc với decorators.

    References

    [1] Fred Baptiste, Python Deep Dive, Part 1

    [2] Brett Slatkin, Effective Python, Item 26

    Authors

    [email protected]

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 3

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 3

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Trong phần 3 này, tôi sẽ giới thiệu một số ví dụ ứng dụng closure để viết code hiệu quả hơn.

    Bài viết này yêu cầu kiến thức tiên quyết về scopes, namespaces, closures trong Python. Nếu bạn chưa tự tin, thì nên đọc trước 2 bài viết dưới đây (theo thứ tự):

    Table of contents

    Closure

    Nhắc lại

    Closure có thể tránh việc lợi dụng các giá trị global và cung cấp một cách thức ẩn dữ liệu (data hiding), cung cấp một giải pháp object-oriented cho vấn đề. Khi chỉ có một vài phương thức được triển khai trong một class, thì closure có thể cung cấp một giải pháp thay thế nhưng thanh lịch hơn. Khi số lượng thuộc tính và phương thức tăng lên nhiều, thì sử dụng class sẽ phù hợp hơn. Các tiêu chí sau cho thấy closure trong Python khi một nested function có tham chiếu một giá trị trong enclosing scope:

    • Tồn tại một nested function (function bên trong function khác)
    • Nested function có tham chiếu đến một giá trị được khai báo bên trong enclosing function.
    • Enclosing function trả về nested function (giá trị được return)

    Nguồn ảnh: Andre Ye

    Averager

    Trong ví dụ này, ta sẽ xây dựng một hàm tính giá trị trung bình của nhiều giá trị sử dụng closure. Hàm này có thể tính giá trị trung bình theo thời gian bằng cách thêm các đối số vào hàm đó mà không cần phải lặp lại việc tính tổng các giá trị trước đó.

    Cách tiếp cận dễ dàng nghĩ đến nhất là sử dụng class trong Python, ở đó ta sẽ sử dụng một biến instance để lưu trữ tổng của dãy số và số số hạng. Sau đó cung cấp cho class đó một method để thêm vào 1 số hạng mới, và trả về giá trị trung bình cộng của dãy số.

    class Averager:
        def __init__(self):
            self._count = 0
            self._total = 0
    
        def add(self, value):
            self._total += value
            self._count += 1
            return self._total / self._count
    
    a = Averager()
    a.add(1) # return 1.0
    a.add(2) # return 1.5
    a.add(3) # return 2.0

    Bằng cách sử dụng closure, ta có thể tận dụng functions trong python để xây dựng được tính năng tương tự việc sử dụng class, nhưng thanh lịch và hiệu quả hơn.

    def averager():
        total = 0
        count = 0
    
        def add(value):
            nonlocal total, count
            total += value
            count += 1
            return 0 if count == 0 else total / count
    
        return add
    
    a = averager()
    a(1) # return 1.0
    a(2) # return 1.5
    a(3) # return 2.0

    Counter

    Áp dụng closure, ta có thể xây dựng 1 bộ đếm, đếm số lần gọi một function mỗi khi function đó chạy. Function này có thể nhận bất kỳ đối số hoặc đối số từ khóa nào.

    def counter(fn):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            print('{0} has been called {1} times'.format(fn.__name__, cnt))
            return fn(*args, **kwargs)
    
        return inner

    Giả sử ta muốn bổ sung thêm việc đếm số lần gọi hàm tính tổng 2 số:

    def add(a, b):
        return a + b

    Ta có thể áp dụng closure như sau:

    count_sum = counter(add))
    count_sum(1, 2) # sum has been called 1 times
    count_sum(3, 5) # sum has been called 2 times

    Sở dĩ hàm count_sum có thể làm được như trên là bởi vì nó đang sở hữu 2 free variables là:

    • fn: tham chiếu đến hàm add
    • cnt: duy trì đếm số lần gọi hàm fn
    count_sum.__code__.co_freevars # ('cnt', 'fn')

    Đến đây, thay vì in ra standard output số lần gọi 1 hàm bất kỳ (hàm add chỉ là 1 ví dụ), ta có thể sử dụng 1 từ điển là global variable lưu trữ các cặp {key: value}. Ở đó, key là tên của hàm và value là số lần gọi hàm. Để làm được điều đó, ta cần sửa đổi một chút ở hàm counter bằng cách bổ sung thêm cho nó 1 đối số là tham chiếu đến từ điển lưu trữ:

    def counter(fn, counters):
        cnt = 0  # số lần chạy fn, khởi tạo là 0
    
        def inner(*args, **kwargs):
            nonlocal cnt
            cnt = cnt + 1
            counters[fn.__name__] = cnt  # counters là nonlocal
            return fn(*args, **kwargs)
    
        return inner
    func_counters = dict() # khởi tạo từ điển
    # đếm số lần chạy hàm add
    counted_add = counter(add, func_counters)
    for i in range(10):
        counted_add(i, i+1)

    Biến func_counters là biến toàn cục, vì vậy ta có thể bổ sung thêm từ khóa là tên của hàm khác vào nó, thử 1 ví dụ, xét hàm nhân 2 số:

    def mult(a, b):
        return a * b
    
    counted_mult = counter(mult, func_counters)
    for i in range(7):
        counted_mult(i, i)

    Biến func_counters lúc này sẽ cho chúng ta biết số lần gọi hàm add và số lần gọi hàm mult

    func_counters ## {'mult': 7, 'add': 10}

    Cả 2 hàm counted_addcounted_mult đều đang giữ 3 free variables:

    • fn: tham chiếu đến hàm cần đếm
    • cnt: duy trì đếm số lần gọi hàm fn
    • counters: tham chiếu đến từ điển lưu trữ thông tin về số lần đếm các hàm

    Hãy thử nghĩ, nếu như, thay vì ta gọi:

    counted_add = counter(add, func_counters)

    Ta gọi như sau:

    add = counter(add, func_counters)

    Lúc này, ta có một hàm add mới, thực sự không khác hàm add lúc đầu về tính năng là tính tổng 2 số. Tuy nhiên sau khi gọi hàm add, lúc này ta còn nhận được thêm thông tin về số lần gọi hàm add được giữ trong biến func_counters.

    Như vậy, hàm counter đóng vai trò như 1 trình trang trí cho hàm add (tương tự với hàm mult), nó bổ sung thêm tính năng cho hàm add nhưng không thay đổi hành vi của hàm ađd (trả về tổng 2 số). Đây là tính chất quan trọng của decorator mà chúng ta sẽ tìm hiểu trong một bài viết sau.

    Use Closures Skilfully

    Closure là một vũ khí mạnh mẽ của Python. Người mới bắt đầu có thể gặp đôi chút khó khăn trong việc áp dụng nó trong việc viết mã. Tuy nhiên, nếu ta có thể hiểu và sử dụng nó một cách thuần thục, thì nó sẽ vô cùng hữu ích.

    Trên thực tế thì decorator trong Python là một sự mở rộng của closure. Chúng ta sẽ bàn về decorator sau, nhưng ai cũng biết rằng hầu hết các framework sử dụng Python cho web development đều sử dụng decorator rất thường xuyên.

    Dưới đây là 2 tips quan trọng sẽ giúp bạn sử dụng closure thuần thục:

    Sử dụng lambda function để đơn giản hóa code

    Xét 1 ví dụ:

    def outer_func():
        name = "Tu Anh"
    
        def print_name():
            print(name)
    
        return print_name
    
    f = outer_func()
    print(outer_func.__closure__) # None
    print(f.__closure__) # (<cell at 0x7f31445b2e90: str object at 0x7f314459c070>,)
    print(f.__closure__[0].cell_contents) # Tu Anh

    Ta có thể làm cho ví dụ trên thanh lịch hơn bằng cách sử dụng lambda function:

    def outer_func():
        name = "Tu Anh"
    
        return lambda _: print(name)
    
    f = outer_func()
    print(outer_func.__closure__) # None
    print(f.__closure__) # (<cell at 0x7f31445a44d0: str object at 0x7f31445b6070>,)
    print(f.__closure__[0].cell_contents) # Tu Anh

    Closures che giấu các biến private hiệu quả hơn

    Trong Python thì không có các từ khóa built-in như là public hay private để kiểm soát khả năng truy cập của các biến. Theo quy ước, chúng ta sử dụng double underscores để định nghĩa một member của 1 class là private. Tuy nhiên, chúng vẫn có thể được truy cập.

    Đôi khi, chúng ta cần bảo vệ mạnh mẽ hơn để ẩn một biến. Và closures có thể giải quyết vấn đề này. Như ví dụ ở trên, thì khó để ta có thể truy cập và thay đổi được giá trị của biến name trong hàm f. Như vậy, biến name dường như đã private hơn.

    References

    [1] Andre Ye, Essential Python Concepts & Structures Any Serious Programmer Needs to Know, Explained

    [2] Fred Baptiste, Python Deep Dive, Part 1

    [3] Yang Zhou, 5 Levels of Understanding Closures in Python

    Authors

    [email protected]

  • Giới thiệu về Transfer learning và Fine-tuning (Phần 2)

    Giới thiệu về Transfer learning và Fine-tuning (Phần 2)

    Tiếp nối bài trước về Transfer Learning, hôm nay chúng ta cùng tìm hiểu về Fine Tuning.

    Mở đầu

    Fine tuning : Thuật ngữ này có thể được dịch là “Tinh chỉnh” – là một quá trình sử dụng một mô hình mạng đã được huấn luyện cho một nhiệm vụ nhất định để thực hiện một nhiệm vụ tương tự. Sở dĩ cách lý giải này có phần giống Transfer Learning – bởi Fine Tuning là một kỹ thuật Transfer Learning mà ! Hãy cùng tìm hiểu xem cụ thể nó là thế nào nhé.

    Khi mô hình của bạn đã hội tụ trên dữ liệu mới, bạn có thể cố gắng giải phóng toàn bộ hoặc một phần của mô hình cơ sở và đào tạo lại toàn bộ mô hình từ đầu đến cuối với tỷ lệ học tập rất thấp.

    Đây là bước cuối cùng tùy chọn có thể mang lại cho bạn những cải tiến gia tăng. Nó cũng có thể dẫn đến tình trạng overfitting – hãy cân nhắc điều đó.

    Điều quan trọng là chỉ thực hiện bước này sau khi mô hình với các lớp đông lạnh đã được huấn luyện để hội tụ. Nếu bạn trộn các lớp trainable được khởi tạo ngẫu nhiên với các lớp trainable chứa các tính năng đã được huấn luyện trước, các lớp được khởi tạo ngẫu nhiên sẽ gây ra các cập nhật gradient rất lớn trong quá trình huấn luyện, điều này sẽ phá hủy các tính năng đã được huấn luyện trước của bạn.

    Một vấn đề quan trọng nữa là là sử dụng tỷ lệ học tập rất thấp ở giai đoạn này, bởi vì bạn đang đào tạo một mô hình lớn hơn nhiều so với trong vòng đào tạo đầu tiên, trên một tập dữ liệu thường rất nhỏ. Do đó, bạn có nguy cơ bị overfitting rất nhanh nếu áp dụng các biện pháp cập nhật trọng lượng lớn. Ở đây, bạn chỉ muốn đọc các trọng số được huấn luyện trước theo cách tăng dần.

    Đây là cách implement fine-tuning toàn bộ mô hình cơ sở:

    # Hủy đóng băng mô hình cơ sở
    base_model.trainable = True
    
    # Quan trọng là phải biên dịch lại mô hình của bạn sau khi thực hiện bất kỳ thay đổi nào đối với thuộc tính `trainable` của bất kỳ lớp bên trong nào
    # Để các thay đổi của bạn được tính đến
    model.compile(optimizer=keras.optimizers.Adam(1e-5),  # Tỉ lệ học rất thấp
                  loss=keras.losses.BinaryCrossentropy(from_logits=True),
                  metrics=[keras.metrics.BinaryAccuracy()])
    
    # Train. Cẩn thận để dừng lại trước khi bị overfit
    model.fit(new_dataset, epochs=10, callbacks=..., validation_data=...)
    

    Lưu ý quan trọng về compile()trainable

    Việc gọi compile() trên một mô hình có nghĩa là "đóng băng" hành vi của mô hình đó. Điều này ngụ ý rằng các giá trị thuộc tính trainable tại thời điểm mô hình được biên dịch nên được bảo toàn trong suốt thời gian tồn tại của mô hình đó, cho đến khi quá trình biên dịch được gọi lại. Do đó, nếu bạn thay đổi bất kỳ giá trị có thể đào tạo nào, hãy đảm bảo gọi lại compile () trên mô hình của bạn để các thay đổi của bạn được tính đến.

    Lưu ý quan trọng về lớp BatchNormalization

    Nhiều mô hình hình ảnh chứa các lớp BatchNormalization. Lớp đó là một trường hợp đặc biệt trên mọi số lượng có thể tưởng tượng được. Dưới đây là một số điều cần ghi nhớ.

    • BatchNormalization chứa 2 trọng lượng không thể đào tạo được cập nhật trong quá trình đào tạo. Đây là các biến theo dõi giá trị trung bình và phương sai của các yếu tố đầu vào.
    • Khi bạn đặt bn_layer.trainable = False, lớp BatchNormalization sẽ chạy ở chế độ suy luận và sẽ không cập nhật thống kê trung bình & phương sai của nó. Điều này không đúng với các lớp khác nói chung, vì khả năng tập tạ & chế độ suy luận / huấn luyện là hai khái niệm trực giao. Nhưng cả hai được gắn với nhau trong trường hợp của lớp BatchNormalization.
    • Khi bạn giải phóng một mô hình có chứa các lớp BatchNormalization để thực hiện tinh chỉnh, bạn nên giữ các lớp BatchNormalization ở chế độ suy luận bằng cách chuyển training = False khi gọi mô hình cơ sở. Nếu không, các bản cập nhật được áp dụng cho các trọng lượng không thể đào tạo sẽ đột ngột phá hủy những gì mà mô hình đã học được.

    Bạn sẽ thấy mẫu này hoạt động trong ví dụ end-to-end ở cuối hướng dẫn này.

    Transfer learning & fine-tuning với một vòng lặp đào tạo tùy chỉnh

    Nếu thay vì fit(), bạn đang sử dụng vòng lặp đào tạo cấp thấp của riêng mình, thì quy trình làm việc về cơ bản vẫn giữ nguyên. Bạn nên cẩn thận chỉ tính đến danh sách model.trainable_weights khi áp dụng cập nhật gradient:

    # Khởi tạo mô hình cơ sở
    base_model = keras.applications.Xception(
        weights='imagenet',
        input_shape=(150, 150, 3),
        include_top=False)
    # Đóng băng mô hình cơ sở
    base_model.trainable = False
    
    # Khởi tạo một mô hình mới on top.
    inputs = keras.Input(shape=(150, 150, 3))
    x = base_model(inputs, training=False)
    x = keras.layers.GlobalAveragePooling2D()(x)
    outputs = keras.layers.Dense(1)(x)
    model = keras.Model(inputs, outputs)
    
    loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)
    optimizer = keras.optimizers.Adam()
    
    # Lặp lại các lô của tập dữ liệu.
    for inputs, targets in new_dataset:
        # Mở GradientTape.
        with tf.GradientTape() as tape:
            # Chuyển tiếp
            predictions = model(inputs)
            # Tính toán giá trị tổn thất cho lô này.
            loss_value = loss_fn(targets, predictions)
    
        # Lấy gradients với độ giảm của trọng số *trainable*.
        gradients = tape.gradient(loss_value, model.trainable_weights)
        # Cập nhật trọng số của mô hình
        optimizer.apply_gradients(zip(gradients, model.trainable_weights))
    

    Một ví dụ từ đầu đến cuối: tinh chỉnh mô hình phân loại hình ảnh trên tập dữ liệu mèo và chó

    Để củng cố những khái niệm này, hãy hướng dẫn bạn qua một ví dụ cụ thể về học tập và tinh chỉnh chuyển giao từ đầu đến cuối. Chúng tôi sẽ tải mô hình Xception, được đào tạo trước trên ImageNet và sử dụng nó trên tập dữ liệu phân loại Kaggle "mèo so với chó".

    Lấy dữ liệu

    Đầu tiên, hãy tìm nạp tập dữ liệu mèo và chó bằng TFDS. Nếu bạn có tập dữ liệu của riêng mình, có thể bạn sẽ muốn sử dụng tiện ích tf.keras.preprocessing.image_dataset_from_directory để tạo các đối tượng tập dữ liệu có nhãn tương tự từ một tập hợp các hình ảnh trên đĩa được lưu trữ vào các thư mục dành riêng cho lớp.

    Học chuyển giao hữu ích nhất khi làm việc với các tập dữ liệu rất nhỏ. Để giữ cho tập dữ liệu của chúng tôi nhỏ, chúng tôi sẽ sử dụng 40% dữ liệu đào tạo ban đầu (25.000 hình ảnh) để đào tạo, 10% để xác thực và 10% để kiểm tra.

    import tensorflow_datasets as tfds
    
    tfds.disable_progress_bar()
    
    train_ds, validation_ds, test_ds = tfds.load(
        "cats_vs_dogs",
        # Reserve 10% for validation and 10% for test
        split=["train[:40%]", "train[40%:50%]", "train[50%:60%]"],
        as_supervised=True,  # Include labels
    )
    
    print("Number of training samples: %d" % tf.data.experimental.cardinality(train_ds))
    print("Number of validation samples: %d" % tf.data.experimental.cardinality(validation_ds))
    print("Number of test samples: %d" % tf.data.experimental.cardinality(test_ds))
    
    Number of training samples: 9305
    Number of validation samples: 2326
    Number of test samples: 2326
    

    Đây là 9 hình ảnh đầu tiên trong tập dữ liệu đào tạo – như bạn có thể thấy, chúng đều có kích thước khác nhau.

    import matplotlib.pyplot as plt
    
    plt.figure(figsize=(10, 10))
    for i, (image, label) in enumerate(train_ds.take(9)):
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(image)
        plt.title(int(label))
        plt.axis("off")
    

    Chúng ta cũng có thể thấy rằng nhãn 1 là "chó" và nhãn 0 là "mèo".

    Chuẩn hóa dữ liệu

    Hình ảnh thô của có nhiều kích cỡ khác nhau. Ngoài ra, mỗi pixel bao gồm 3 giá trị integer 0 đến 255 (giá trị RGB). Đây không phải là một sự phù hợp tuyệt vời cho mạng nơ-ron. Chúng ta cần làm 2 việc:

    • Chuẩn hóa thành kích thước hình ảnh cố định. Chúng ta chọn 150×150.
    • Chuẩn hóa các giá trị pixel từ -1 đến 1. Chúng tôi sẽ thực hiện việc này bằng cách sử dụng lớp Chuẩn hóa như một phần của chính mô hình.

    Nói chung, bạn nên phát triển các mô hình lấy dữ liệu thô làm đầu vào, trái ngược với các mô hình lấy dữ liệu đã được xử lý trước. Lý do là, nếu mô hình của bạn yêu cầu dữ liệu được xử lý trước, bất kỳ khi nào bạn xuất mô hình của mình để sử dụng ở nơi khác (trong trình duyệt web, trong ứng dụng dành cho thiết bị di động), bạn sẽ cần phải thực hiện lại cùng một quy trình xử lý trước. Điều này trở nên rất phức tạp rất nhanh chóng. Vì vậy, chúng ta nên thực hiện ít tiền xử lý nhất có thể trước khi đưa vào mô hình.

    Ở đây, chúng ta sẽ thực hiện thay đổi kích thước hình ảnh trong đường ống dữ liệu (vì mạng nơ-ron sâu chỉ có thể xử lý các lô dữ liệu liền kề) và chúng tôi sẽ thực hiện điều chỉnh tỷ lệ giá trị đầu vào như một phần của mô hình, khi chúng tôi tạo nó.

    Hãy thay đổi kích thước hình ảnh thành 150×150:

    size = (150, 150)
    
    train_ds = train_ds.map(lambda x, y: (tf.image.resize(x, size), y))
    validation_ds = validation_ds.map(lambda x, y: (tf.image.resize(x, size), y))
    test_ds = test_ds.map(lambda x, y: (tf.image.resize(x, size), y))
    

    Bên cạnh đó, hãy tập hợp dữ liệu và sử dụng bộ nhớ đệm & tìm nạp trước để tối ưu hóa tốc độ tải.

    batch_size = 32
    
    train_ds = train_ds.cache().batch(batch_size).prefetch(buffer_size=10)
    validation_ds = validation_ds.cache().batch(batch_size).prefetch(buffer_size=10)
    test_ds = test_ds.cache().batch(batch_size).prefetch(buffer_size=10)
    

    Sử dụng tăng dữ liệu ngẫu nhiên

    Khi bạn không có tập dữ liệu hình ảnh lớn, bạn nên đưa tính đa dạng mẫu vào một cách giả tạo bằng cách áp dụng các phép biến đổi ngẫu nhiên nhưng thực tế cho hình ảnh huấn luyện, chẳng hạn như lật ngang ngẫu nhiên hoặc xoay ngẫu nhiên nhỏ. Điều này giúp mô hình hiển thị các khía cạnh khác nhau của dữ liệu đào tạo trong khi làm chậm quá trình overfitting.

    from tensorflow import keras
    from tensorflow.keras import layers
    
    data_augmentation = keras.Sequential(
        [layers.RandomFlip("horizontal"), layers.RandomRotation(0.1),]
    )
    

    Hãy hình dung hình ảnh đầu tiên của lô đầu tiên trông như thế nào sau nhiều lần biến đổi ngẫu nhiên:

    import numpy as np
    
    for images, labels in train_ds.take(1):
        plt.figure(figsize=(10, 10))
        first_image = images[0]
        for i in range(9):
            ax = plt.subplot(3, 3, i + 1)
            augmented_image = data_augmentation(
                tf.expand_dims(first_image, 0), training=True
            )
            plt.imshow(augmented_image[0].numpy().astype("int32"))
            plt.title(int(labels[0]))
            plt.axis("off")
    

    Xây dựng một mô hình

    Bây giờ chúng ta hãy xây dựng một mô hình theo kế hoạch chi tiết đã giải thích trước đó.

    Lưu ý rằng:

    • Thêm một lớp Rescaling để chia tỷ lệ các giá trị đầu vào (ban đầu trong phạm vi [0, 255]) thành phạm vi [-1, 1].
    • Thêm một lớp Dropout trước lớp phân loại, để chính quy hóa.
    • Đảm bảo training = False khi gọi mô hình cơ sở, để nó chạy ở chế độ suy luận, do đó thống kê batchnorm không được cập nhật ngay cả sau khi chúng tôi giải phóng mô hình cơ sở để fine-tuning.
    base_model = keras.applications.Xception(
        weights="imagenet",  # Load weights pre-trained on ImageNet.
        input_shape=(150, 150, 3),
        include_top=False,
    )  # Do not include the ImageNet classifier at the top.
    
    # Freeze the base_model
    base_model.trainable = False
    
    # Create new model on top
    inputs = keras.Input(shape=(150, 150, 3))
    x = data_augmentation(inputs)  # Apply random data augmentation
    
    # Pre-trained Xception weights requires that input be scaled
    # from (0, 255) to a range of (-1., +1.), the rescaling layer
    # outputs: `(inputs * scale) + offset`
    scale_layer = keras.layers.Rescaling(scale=1 / 127.5, offset=-1)
    x = scale_layer(x)
    
    # The base model contains batchnorm layers. We want to keep them in inference mode
    # when we unfreeze the base model for fine-tuning, so we make sure that the
    # base_model is running in inference mode here.
    x = base_model(x, training=False)
    x = keras.layers.GlobalAveragePooling2D()(x)
    x = keras.layers.Dropout(0.2)(x)  # Regularize with dropout
    outputs = keras.layers.Dense(1)(x)
    model = keras.Model(inputs, outputs)
    
    model.summary()
    
    Model: "model"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    input_5 (InputLayer)         [(None, 150, 150, 3)]     0         
    _________________________________________________________________
    sequential_3 (Sequential)    (None, 150, 150, 3)       0         
    _________________________________________________________________
    rescaling (Rescaling)        (None, 150, 150, 3)       0         
    _________________________________________________________________
    xception (Functional)        (None, 5, 5, 2048)        20861480  
    _________________________________________________________________
    global_average_pooling2d (Gl (None, 2048)              0         
    _________________________________________________________________
    dropout (Dropout)            (None, 2048)              0         
    _________________________________________________________________
    dense_7 (Dense)              (None, 1)                 2049      
    =================================================================
    Total params: 20,863,529
    Trainable params: 2,049
    Non-trainable params: 20,861,480
    _________________________________________________________________
    

    Đào tạo lớp trên cùng

    model.compile(
        optimizer=keras.optimizers.Adam(),
        loss=keras.losses.BinaryCrossentropy(from_logits=True),
        metrics=[keras.metrics.BinaryAccuracy()],
    )
    
    epochs = 20
    model.fit(train_ds, epochs=epochs, validation_data=validation_ds)
    
    Epoch 1/20
    291/291 [==============================] - 133s 451ms/step - loss: 0.1670 - binary_accuracy: 0.9267 - val_loss: 0.0830 - val_binary_accuracy: 0.9716
    Epoch 2/20
    291/291 [==============================] - 135s 465ms/step - loss: 0.1208 - binary_accuracy: 0.9502 - val_loss: 0.0768 - val_binary_accuracy: 0.9716
    Epoch 3/20
    291/291 [==============================] - 135s 463ms/step - loss: 0.1062 - binary_accuracy: 0.9572 - val_loss: 0.0757 - val_binary_accuracy: 0.9716
    Epoch 4/20
    291/291 [==============================] - 137s 469ms/step - loss: 0.1024 - binary_accuracy: 0.9554 - val_loss: 0.0733 - val_binary_accuracy: 0.9725
    Epoch 5/20
    291/291 [==============================] - 137s 470ms/step - loss: 0.1004 - binary_accuracy: 0.9587 - val_loss: 0.0735 - val_binary_accuracy: 0.9729
    Epoch 6/20
    291/291 [==============================] - 136s 467ms/step - loss: 0.0979 - binary_accuracy: 0.9577 - val_loss: 0.0747 - val_binary_accuracy: 0.9708
    Epoch 7/20
    291/291 [==============================] - 134s 462ms/step - loss: 0.0998 - binary_accuracy: 0.9596 - val_loss: 0.0706 - val_binary_accuracy: 0.9725
    Epoch 8/20
    291/291 [==============================] - 133s 457ms/step - loss: 0.1029 - binary_accuracy: 0.9592 - val_loss: 0.0720 - val_binary_accuracy: 0.9733
    Epoch 9/20
    291/291 [==============================] - 135s 466ms/step - loss: 0.0937 - binary_accuracy: 0.9625 - val_loss: 0.0707 - val_binary_accuracy: 0.9721
    Epoch 10/20
    291/291 [==============================] - 137s 472ms/step - loss: 0.0967 - binary_accuracy: 0.9580 - val_loss: 0.0720 - val_binary_accuracy: 0.9712
    Epoch 11/20
    291/291 [==============================] - 135s 463ms/step - loss: 0.0961 - binary_accuracy: 0.9612 - val_loss: 0.0802 - val_binary_accuracy: 0.9699
    Epoch 12/20
    291/291 [==============================] - 134s 460ms/step - loss: 0.0963 - binary_accuracy: 0.9638 - val_loss: 0.0721 - val_binary_accuracy: 0.9716
    Epoch 13/20
    291/291 [==============================] - 136s 468ms/step - loss: 0.0925 - binary_accuracy: 0.9635 - val_loss: 0.0736 - val_binary_accuracy: 0.9686
    Epoch 14/20
    291/291 [==============================] - 138s 476ms/step - loss: 0.0909 - binary_accuracy: 0.9624 - val_loss: 0.0766 - val_binary_accuracy: 0.9703
    Epoch 15/20
    291/291 [==============================] - 136s 467ms/step - loss: 0.0949 - binary_accuracy: 0.9598 - val_loss: 0.0704 - val_binary_accuracy: 0.9725
    Epoch 16/20
    291/291 [==============================] - 133s 456ms/step - loss: 0.0969 - binary_accuracy: 0.9586 - val_loss: 0.0722 - val_binary_accuracy: 0.9708
    Epoch 17/20
    291/291 [==============================] - 135s 464ms/step - loss: 0.0913 - binary_accuracy: 0.9635 - val_loss: 0.0718 - val_binary_accuracy: 0.9716
    Epoch 18/20
    291/291 [==============================] - 137s 472ms/step - loss: 0.0915 - binary_accuracy: 0.9639 - val_loss: 0.0727 - val_binary_accuracy: 0.9725
    Epoch 19/20
    291/291 [==============================] - 134s 460ms/step - loss: 0.0938 - binary_accuracy: 0.9631 - val_loss: 0.0707 - val_binary_accuracy: 0.9733
    Epoch 20/20
    291/291 [==============================] - 134s 460ms/step - loss: 0.0971 - binary_accuracy: 0.9609 - val_loss: 0.0714 - val_binary_accuracy: 0.9716
    
    <keras.callbacks.History at 0x7f4494e38f70>
    

    Thực hiện một vòng tinh chỉnh toàn bộ mô hình

    Cuối cùng, hãy giải phóng mô hình cơ sở và đào tạo toàn bộ mô hình từ đầu đến cuối với tỷ lệ học tập thấp.

    Quan trọng là, mặc dù mô hình cơ sở trở nên có thể huấn luyện được, nhưng nó vẫn đang chạy ở chế độ suy luận vì chúng ta đã đặt training=False khi gọi nó khi chúng ta xây dựng mô hình. Điều này có nghĩa là các lớp chuẩn hóa hàng loạt bên trong sẽ không cập nhật thống kê hàng loạt của chúng. Nếu họ làm vậy, họ sẽ phá hủy các đại diện mà mô hình đã học cho đến nay.

    base_model.trainable = True
    model.summary()
    
    model.compile(
        optimizer=keras.optimizers.Adam(1e-5),  # Low learning rate
        loss=keras.losses.BinaryCrossentropy(from_logits=True),
        metrics=[keras.metrics.BinaryAccuracy()],
    )
    
    epochs = 10
    model.fit(train_ds, epochs=epochs, validation_data=validation_ds)
    
    Model: "model"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    input_5 (InputLayer)         [(None, 150, 150, 3)]     0         
    _________________________________________________________________
    sequential_3 (Sequential)    (None, 150, 150, 3)       0         
    _________________________________________________________________
    rescaling (Rescaling)        (None, 150, 150, 3)       0         
    _________________________________________________________________
    xception (Functional)        (None, 5, 5, 2048)        20861480  
    _________________________________________________________________
    global_average_pooling2d (Gl (None, 2048)              0         
    _________________________________________________________________
    dropout (Dropout)            (None, 2048)              0         
    _________________________________________________________________
    dense_7 (Dense)              (None, 1)                 2049      
    =================================================================
    Total params: 20,863,529
    Trainable params: 20,809,001
    Non-trainable params: 54,528
    _________________________________________________________________
    Epoch 1/10
    291/291 [==============================] - 567s 2s/step - loss: 0.0749 - binary_accuracy: 0.9689 - val_loss: 0.0605 - val_binary_accuracy: 0.9776
    Epoch 2/10
    291/291 [==============================] - 551s 2s/step - loss: 0.0559 - binary_accuracy: 0.9770 - val_loss: 0.0507 - val_binary_accuracy: 0.9798
    Epoch 3/10
    291/291 [==============================] - 545s 2s/step - loss: 0.0444 - binary_accuracy: 0.9832 - val_loss: 0.0502 - val_binary_accuracy: 0.9807
    Epoch 4/10
    291/291 [==============================] - 558s 2s/step - loss: 0.0365 - binary_accuracy: 0.9874 - val_loss: 0.0506 - val_binary_accuracy: 0.9807
    Epoch 5/10
    291/291 [==============================] - 550s 2s/step - loss: 0.0276 - binary_accuracy: 0.9890 - val_loss: 0.0477 - val_binary_accuracy: 0.9802
    Epoch 6/10
    291/291 [==============================] - 588s 2s/step - loss: 0.0206 - binary_accuracy: 0.9916 - val_loss: 0.0444 - val_binary_accuracy: 0.9832
    Epoch 7/10
    291/291 [==============================] - 542s 2s/step - loss: 0.0206 - binary_accuracy: 0.9923 - val_loss: 0.0502 - val_binary_accuracy: 0.9828
    Epoch 8/10
    291/291 [==============================] - 544s 2s/step - loss: 0.0153 - binary_accuracy: 0.9939 - val_loss: 0.0509 - val_binary_accuracy: 0.9819
    Epoch 9/10
    291/291 [==============================] - 548s 2s/step - loss: 0.0156 - binary_accuracy: 0.9934 - val_loss: 0.0610 - val_binary_accuracy: 0.9807
    Epoch 10/10
    291/291 [==============================] - 546s 2s/step - loss: 0.0176 - binary_accuracy: 0.9936 - val_loss: 0.0561 - val_binary_accuracy: 0.9789
    
    <keras.callbacks.History at 0x7f4495056040> 
    

    Sau 10 epochs, fine-tuning mang lại cho chúng ta một cải tiến tốt đẹp ở đây.

    Kết bài

    Trên đây chúng ta đã tìm hiểu kỹ thuật Fine-tuning. Cảm ơn các bạn đã giành thời gian theo dõi. Thân ái !

    Tham khảo https://keras.io/guides/

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 2

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 2

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Bài viết này yêu cầu kiến thức tiên quyết về scopes, namespace trong Python. Nếu bạn chưa tự tin, vui lòng đọc trước Phần 1

    Table of contents

    Free Variables and Closures

    Nhắc lại rằng: Các functions được xác định bên trong function khác có thể truy cập các biến bên ngoài (nonlocal)

    def outer():
        x = 'python'
        def inner():
            # x trỏ đến cùng một object mà biến x bên ngoài trỏ tới.
            print("{0} rocks!".format(x))
        inner()
    outer() # python rocks! --> Đây được gọi là một closure.

    Biến nonlocal x trong hàm inner được gọi là free variable. Khi chúng ta xem xét hàm inner, chúng ta thực sự đang thấy:

    • hàm inner
    • free variable x (đang có giá trị là ‘python’)

    Xin lưu ý rằng biến x trong hàm inner không thuộc local scope của hàm đó, mà nó nằm ở một nơi khác. Nhãn x này và nhãn x thuộc hàm outer liên kết lại với nhau, được gọi là closure.

    Returning the inner function

    Vậy điều gì sẽ xảy ra nếu như chúng ta không gọi hàm inner() bên trong hàm outer() mà thay vào đó, ta return nó. Khi gọi hàm outer(), hàm inner sẽ được tạo, và outer trả về hàm inner. Khi đó, closure nói trên vẫn đang còn tồn tại, chúng không bị mất đi. Vì vậy, khi gọi hàm outer(), trả về hàm inner, thực sự là chúng ta đang trả về một closure.
    Chúng ta có thể gán giá trị trả về từ hàm outer() cho một tên biến, ví dụ:

    fn = outer() # fn là closure
    fn() # python rocks!

    Khi chúng ta gọi hàm fn(), tại thời điểm đó – Python xác định giá trị của x trong một extended scope. Lưu ý rằng, hàm outer() đã chạy xong và đã kết thúc trước khi gọi hàm fn() –> scope của hàm outer đã được giải phóng. Vậy tại sao khi gọi hàm fn(), chúng ta vẫn nhận về được giá trị ‘python rocks!’ !!? –> closure.
    Thật magic! Để hiểu rõ hơn về closure, bạn hãy uống một chén trà rồi ngồi đọc tiếp nhé ;).

    Python Cells and Multi-Scoped Variables

    Xét ví dụ đơn giản sau:

    def outer():
        x = 'tuanh'
        def inner():
            print(x)
        return inner

    Giá trị của biến x được chia sẻ giữa 2 scope:

    • outer
    • closure

    Nhãn (label, name) x nằm trong 2 scope khác nhau nhưng luôn luôn refer tới cùng 1 giá trị. Python làm điều này bằng cách sử dụng một đối tượng trung gian, cell object.

    cell object đóng vai trò trung gian, và x sẽ tham chiếu gián tiếp đến đối tượng có giá trị ‘tuanh’. Trên thực tế, cả 2 biến x (trong outer và inner) đều trỏ đến cùng một cell object. Và khi chúng ta request giá trị của biến, Python thực hiện “double-hop” để lấy về giá trị cuối cùng.
    Bây giờ, chúng ta đã hiểu tại sao khi hàm outer() kết thúc, chúng ta vẫn có thể lấy được giá trị của biến x trong hàm inner rồi chứ.

    Closures

    Đến đây, chúng ta có thể nghĩ về closure như là một function + extended scope – scope mà chứa free variables.
    Giá trị của free variable là object mà cell trỏ tới. Mỗi khi function trong closure được gọi và free variable được tham chiếu:

    • Python tìm kiếm cell object, và sau đó bất kỳ cái gì cell đang trỏ tới.
    Nguồn: Fred Baptiste (Python Deep Dive – Functional)

    Introspection

    Chúng ta tiếp tục sử dụng ví dụ như trước:

    Nguồn: Fred Baptiste (Python Deep Dive – Functional)

    (more…)

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 1

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 1

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Table of contents

    Global and Local Scopes

    Scopes and Namespaces

    Khi một đối tượng được gán cho một biến (ví dụ: a = 100) thì biến đó trỏ đến một object nào đó và chúng ta nói rằng biến (name) đó được liên kết với đối tượng đó. Khi đó, object có thể được truy cập từ một số nơi trong code của chúng ta, sử dụng name (tên biến) nói trên.
    Tuy nhiên, hãy nhớ rằng tên biến và binding của nó (name và object) chỉ tồn tại trong một phần cụ thể của mã nguồn của chúng ta; phần mã nguồn mà ở đó name/binding được xác định – được gọi là lexical scope của các biến. Các bindings này được lưu trữ trong namespaces (mỗi scope có namespace riêng của nó).

    The Global Scope

    Global scope về cơ bản là module scope. Nó chỉ nằm trong một file .py duy nhất. Trong Python thì KHÔNG có khái niệm global scope (qua tất cả các mô đun trong toàn bộ ứng dụng) thực sự. Chỉ có một số ngoại lệ đó là có một số đối tượng built-in, được sử dụng toàn cục, chẳng hạn như: True, False, None, dict, print.

    Các biến built-in và global có thể được sử dụng bất kỳ đâu trong mô đun của chúng ta, kể cả trong các hàm.

    Global scopes được nested bên trong built-in scope.

    Nếu bạn tham chiếu một tên biến bên trong một scope và Python không tìm thấy nó trong không gian tên của scope đó –> Python sẽ tìm nó bên trong không gian tên của enclosing scope. Ví dụ trong Module1 bạn sử dụng đến nhãn True, Python sẽ tìm True bên trong không gian tên của built-in scope.

    The Local Scope

    Khi chúng ta tạo một function, chúng ta có thể tạo các tên biến bên trong function đó (sử dụng các đối số là ví dụ). Các biến được tạo bên trong function sẽ không được tạo cho đến khi function được gọi.

    Lưu ý: Giả sử chúng ta có function func1 bên trong module. Thì khi load module, Python sẽ compile mọi thứ và func1 sẽ nằm trong namespace của module đang được load. Tuy nhiên mọi thứ bên trong function sẽ chưa được tạo cho tới tận khi chúng được gọi bởi lời gọi hàm.

    def func1(a, b):
        # do something
        pass

    Mỗi khi function được gọi thì một scope mới sẽ được tạo. Và các biến được xác định bên trong function sẽ được gán cho scope đó – được gọi là function local scope. Lưu ý rằng đối tượng thực sự được tạo ra mà một biến trong hàm tham chiếu đến có thể khác nhau trong các lần hàm được gọi (đây là lý do tại sao đệ quy hoạt động!).

    Nested Scopes

    Các scope thường được nested trong các scope khác.
    Khi yêu cầu truy cập vào một object mà một biến tham chiếu đến. Ví dụ:

    print(a)

    Python sẽ tìm object được bound tới biến a như sau:

    • Đầu tiên, tìm trong local scope hiện tại. Nếu không thấy,
    • Lần lượt tìm tiếp lên các scope ‘bao bọc’ scope đang tìm.

    Thêm một ví dụ:

    # module1.py
    a = 10 # a thuộc global scope
    def myFunc(b):
        print(True) # print và True thuộc built-in scope
        print(a) # a thuộc global scope
        print(b) # b thuộc local scope
    
    myFunc(300) # một local scope mới được tạo, b trỏ đến đối tượng lưu trữ 300
    myFunc('a') # thêm một local scope nữa được tạo, b trỏ đến đối tượng lưu trữ 'a'

    The global keyword

    Khi chúng ta truy xuất một giá trị của một biến global bên trong một function. Python sẽ tìm kiến nó theo chuỗi các không gian tên tăng giần: local -> global -> built-in

    Điều gì sẽ xảy ra nếu như chúng ta sửa giá trị của một biến global bên trong một function ?

    a = 0
    def myFunc():
        a = 100 # Python sẽ hiểu rằng đây là biến local tại compile-time
        print(a)
    myFunc() ## 100
    print(a) ## 0

    Chúng ta có thể bảo Python rằng 1 biến được scoped bên trong global scope khi sử dụng nó bên trong một local scope (hàm) bằng cách sử dụng từ khóa global.

    a = 0
    def myFunc():
        global a
        a = 100
        print(a)
    myFunc() ## 100
    print(a) ## 100

    Global and Local Scoping

    Khi Python gặp một định nghĩa hàm tại compile-time, nó sẽ scan bất kỳ nhãn (biến) nào có giá trị assigned cho chúng (anywhere trong function). Nếu nhãn đó không được chỉ định là global thì nó sẽ là local. Các biến được tham chiếu nhưng not assigned một giá trị ở bất kỳ đâu trong function sẽ not be local, và Python sẽ, tại run-time, tìm kiếm chúng trong enclosing scopes.
    Ví dụ 1:

    # vidu1.py
    a = 10
    def func1():
        print(a) ## a chỉ được tham chiếu đến trong function chứ không được gán -> tại compile-time, a là non-local
    def func2():
        a = 100 ## a được gán trong function -> tại compile-time, a là local
    
    def func3():
        global a
        a = 100 ## a được gán và được chỉ định global -> tại compile-time, a là global

    Ví dụ 2:

    # vidu2.py
    def func4():
        print(a)
        a = 100 # tại compile-time, a là local
    
    # khi gọi hàm func4()
    # print(a) sẽ dẫn đến một run-time error bởi vì a là local,
    # và chúng ta đang tham chiếu đến nó trước khi chúng ta gán một
    # giá trị cho nó.
    func4()

    Code

    Tham khảo thêm các ví dụ về Global, local scopes tại:
    Click here

    Nonlocal Scopes

    Inner Functions

    Chúng ta có thể định nghĩa các hàm bên trong hàm khác, như sau:

    def outer_func():
        # some code here
        def inner_func():
            # some code here
    
        inner_func()
    outer_func()

    Cả hai hàm đều có quyền truy cập vào global scope, built-in scope và local scope tương ứng của chúng. Nhưng inner function cũng có thể truy cập vào enclosing scope của nó – ở đây là outer function.
    Scope mà không là local, cũng không là global, thì được gọi là nonlocal scope

    Modifying nonlocal variables

    Xét ví dụ sau:

    # vd.py
    def outer_func():
        x = 'TuAnh'
        def inner_func():
            x = 'HuuLinh'
        inner_func()
        print(x)
    outer_func() ## TuAnh

    Khi inner_func được compiled, python nhìn vào phép gán tới biến x -> nó xác định được biến x là local của hàm inner_func. Đứng ở góc độ inner_func thì biến x trong outer_func là nonlocal. Hai biến này trỏ đến 2 đối tượng khác nhau, vì vậy khi gọi outer_func() sẽ in ra màn hình chuỗi ‘TuAnh’ thay vì ‘HuuLinh’.

    Vậy làm cách nào để có thể sửa được biến x của hàm outer_func bên trong hàm inner_func. Rất đơn giản, giống như các biến global, chúng ta cần khai báo rõ ràng nonlocal với biến x bên trong hàm inner_func, như sau:

    def outer_func():
        x = 'TuAnh'
        def inner_func():
            nonlocal x
            x = 'HuuLinh'
        inner_func()
        print(x)
    outer_func() ## HuuLinh

    Nonlocal Variables

    Bất cứ khi nào Python nói rằng một biến là nonlocal, nó sẽ tìm kiếm trong enclosing local scopes lần lượt từ trong ra ngoài, cho tới khi bắt gặp lần đầu tên biến được chỉ định.
    Lưu ý: Python chỉ tìm trong local scopes, nó sẽ KHÔNG tìm trong global scope.

    Xem xét các ví dụ sau đây:

    # vd1.py
    def outer():
        x = 'tuanh'
        def inner1():
            x = 'python'
            def inner2():
                nonlocal x
                x = 'huulinh'
            print('inner(before): ', x)  # python
            inner2()
            print('inner(after): ', x)   # huulinh
        inner1()
        print('outer: ', x)              # tuanh
    outer()
    # vd2.py
    def outer():
        x = 'tuanh'
        def inner1():
            nonlocal x
            x = 'python'
            def inner2():
                nonlocal x
                x = 'huulinh'
            print('inner(before): ', x)  # python
            inner2()
            print('inner(after): ', x)   # huulinh
        inner1()
        print('outer: ', x)              # huulinh
    outer()
    # vd3.py
    x = 1000
    def outer():
        x = 'tuanh'
        def inner1():
            nonlocal x
            x = 'python'
            def inner2():
                global x
                x = 'huulinh'
            print('inner(before): ', x)  # python
            inner2()
            print('inner(after): ', x)   # python
        inner1()
        print('outer: ', x)              # python
    outer()
    print(x)                             # huulinh

    Authors

    [email protected]

  • Cách cấu hình Gitlab CI chỉ kiểm tra các file thay đổi với merge request

    Cách cấu hình Gitlab CI chỉ kiểm tra các file thay đổi với merge request

    Chúng ta sẽ thường hay cấu hình chạy kiểm tra code quality cho tất các các nhánh và merge request trên Gitlab CI. Nhưng Gitlab runner job sẽ chạy scan toàn bộ source code nên sẽ mất thời gian đối với merge request. Ngoài ra đôi khi do source code cũ của khách hàng lỏm sẽ dẫn tới lúc nào merge request đó cũng sẽ bị failed.

    Dưới đây là sample về Gitlab CI chạy python lint với cấu hình chạy cho nhánh develop và merge request.

    pylint:
      stage: lint
      before_script:
        - mkdir -p public/badges public/lint
        - echo undefined > public/badges/$CI_JOB_NAME.score
        - pip install pylint-gitlab
      script:
        - pip install -r requirements.txt
        - pylint --rcfile=.pylintrc --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/.venv/**") | tee /tmp/pylint.txt
        - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
        - pylint --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > codeclimate.json
        - pylint --exit-zero --output-format=pylint_gitlab.GitlabPagesHtmlReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > public/lint/index.html
      after_script:
        - anybadge --overwrite --label $CI_JOB_NAME --value=$(cat public/badges/$CI_JOB_NAME.score) --file=public/badges/$CI_JOB_NAME.svg 4=red 6=orange 8=yellow 10=green
        - |
          echo "Your score is: $(cat public/badges/$CI_JOB_NAME.score)"
      artifacts:
        paths:
          - public
        reports:
          codequality: codeclimate.json
        when: always
      only:
        - develop
        - merge_requests
      tags:
        - docker
    

    Để giải quyết các vấn đề trên và có thể tập trung vào chất lượng code của các bạn member tốt hơn, chúng ta có thể tạo thêm 1 config mới cho merge request event. Với config mới chỉ cần kiểm tra code quality trên các file thay đổi là được.

    pylint_merge_request:
      stage: lint
      before_script:
        - pip install pylint-gitlab
      script:
        - echo CI_COMMIT_SHA=${CI_COMMIT_SHA}
        - echo CI_MERGE_REQUEST_TARGET_BRANCH_NAME=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
        - git fetch origin ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
        - FILES=$(git diff --name-only ${CI_COMMIT_SHA} origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME} | grep '\.py'$)
        - echo "Changed files are $FILES"
        - pip install -r requirements.txt
        - pylint --rcfile=.pylintrc --output-format=text $FILES | tee /tmp/pylint.txt
      only:
        - merge_requests
      tags:
        - docker
    

    Hi vọng nó hữu ích với mọi người và có thể apply nó vào dự án của mình.

  • Mô phỏng AWS Lambda & API Gateway bằng Serverless Offline

    Mô phỏng AWS Lambda & API Gateway bằng Serverless Offline

    Khi phát triển ứng dùng bằng AWS Lambda không phải lúc nào chúng ta cũng có thể phát triển trực tiếp trên AWS được. Do đó việc giả lập môi trường AWS để có thể chạy được Lambda và API Gateway là cần thiết. Nó không chỉ giúp chúng ta có thể học mà còn giúp cho quá trình phát triển nhanh hơn. Trong bài viết này tôi sẽ hướng dẫn các bạn giả lập AWS Lambda và API Gateway bằng Serverless Offline

    Các công cụ cần thiết

    Trước tiên bạn cần cài đặt các tool cần thiết, bạn có thể tham khảo hướng dẫn cài đặt trong các bài viết sau:

    Bạn có thể dùng lệnh sau để cài serverless

    hieunv@HieuNV lambda % yarn global add serverless
    yarn global v1.22.0
    [1/4] ?  Resolving packages...
    [2/4] ?  Fetching packages...
    [3/4] ?  Linking dependencies...
    [4/4] ?  Building fresh packages...
    success Installed "[email protected]" with binaries:
          - serverless
          - slss
          - sls
    ✨  Done in 14.23s.
    

    Tạo một project mới

    • Tạo project với yarn
    hieunv@HieuNV hieunv % mkdir lambda
    hieunv@HieuNV hieunv % cd lambda
    hieunv@HieuNV lambda % yarn init
    yarn init v1.22.0
    question name (lambda):
    question version (1.0.0):
    question description:
    question entry point (index.js):
    question repository url:
    question author:
    question license (MIT):
    question private:
    success Saved package.json
    ✨  Done in 3.53s.
    
    • Cài đặt serverless-offline
    hieunv@HieuNV lambda % yarn add serverless-offline -D
    
    • Cài đặt serverless-python-requirements để viết lambda handler bằng python
    hieunv@HieuNV lambda % yarn add serverless-python-requirements -D
    

    Cấu hình serverless.yml

    serverless.yml

    service: lambda
    
    frameworkVersion: '>=1.1.0 <2.0.0'
    
    provider:
      name: aws
      runtime: python3.7
    custom:
      serverless-offline:
        port: 4000
    plugins:
      - serverless-offline
      - serverless-python-requirements
    

    Cấu hình lambda handler đầu tiên trong serverless.yml

    Chúng ta tạo một Rest API sử dụng lambda bằng cách thêm đoạn sau vào file serverless.yml

    functions:
      test:
        handler: src.api.test.lambda_handler
        events:
          - http:
              method: get
              path: api/test
              cors: true
    

    Ở đây chúng ta tạo ra một Rest API với phướng thức GET và path /api/test. Các bạn nhìn thấy handler: src.api.test.lambda_handler đúng không. Đây là cấu hình hàm lamda sẽ được gọi bởi API Gateway

    Viết code cho lambda handler

    src/api/test.py

    import json
    
    
    def lambda_handler(event, context):
        headers = {"Access-Control-Allow-Origin": "*", "Accept": "application/json"}
        return {
            "statusCode": 200,
            "headers": headers,
            "body": json.dumps({"status": "success", "data": {}}),
        }
    

    Tạo script để run server

    Thêm đoạn sau vào package.json

        "scripts": {
            "start": "sls offline start"
        },
    

    Giờ thì chạy thôi nào các thanh niên

    hieunv@HieuNV lambda % yarn start
    yarn run v1.22.0
    $ sls offline start
    Serverless: Starting Offline: dev/us-east-1.
    
    Serverless: Routes for test:
    Serverless: GET /api/test
    Serverless: POST /{apiVersion}/functions/lambda-dev-test/invocations
    
    Serverless: Offline [HTTP] listening on http://localhost:4000
    Serverless: Enter "rp" to replay the last request
    

    Dùng Postman để call api vừa tạo nhé:

    Cám ơn các bạn đã theo dõi bài viết. Hy vọng bài viết có thể giúp các bạn tiếp tục học và làm việc cùng với AWS Lambda và API Gateway trong các dự án của mình.

  • Quản lý python packages như thế nào cho đúng

    Quản lý python packages như thế nào cho đúng

    Maven dùng pom để quản lý packages, Node thì có packages.json. Anh em python thì quản lý python packages bằng pip (Package installer for Python). Tuy nhiên khi sử dụng pip sẽ gặp phải tình huống các dự án khác nhau sử dụng dánh sách packages khác nhau. Vấn đề lớn hơn nữa có thể xảy ra tình huống 2 dự án nào đó sử dụng cùng package ở hai phiên bản khách nhau. Trong bài viết này tôi sẽ hướng dẫn các bạn cách quản lý python packages cho các dự án khác nhau mà không bị phụ thuộc vào nhau. Chúng ta cùng bắt đầu nhé.

    Kiểm tra tình trạng hoạt động của brew nào:

    hieunv@HieuNV ~ % brew update && brew upgrade
    Updated 1 tap (homebrew/core).
    ==> Updated Formulae
    abcm2ps             consul              grails              pjproject
    allure              dps8m               graphicsmagick      rancid
    appstream-glib      erlang              i2p                 ratfor
    byteman             flyway              jruby               stress-ng
    camlp5              folly               libgphoto2          zydis
    cargo-instruments   fonttools           libjwt
    cfengine            gmsh                mill
    clblast             gptfdisk            mongoose
    

    Cài đặt python 3

    brew install python
    

    Mặc định MacOS sử dụng python 2

    hieunv@HieuNV ~ % python -V
    Python 2.7.16
    hieunv@HieuNV ~ % python3 -V
    Python 3.7.0
    

    Các bạn cần thêm đoạn sau vào ~/.zshrc.

    export PATH="/usr/local/opt/python/libexec/bin:/usr/local/sbin:$PATH"
    

    Nếu đang mở Terminal bạn cần đóng Terminal lại rồi kiểm tra lại python version

    hieunv@HieuNV ~ % python -V
    Python 3.7.6
    hieunv@HieuNV ~ % pip -V
    pip 19.3.1 from /usr/local/lib/python3.7/site-packages/pip (python 3.7)
    

    Cài đặt virtualenvvirtualenvwrapper

    Với python các packages không được cài đặt cục bộ giống như node. Do đó chúng ta cần tạo ra các môi trường khác nhau với các packages khác nhau để sử dụng cho các dự án khác nhau.

    Cài đặt virtualenv

    pip install virtualenv
    

    Cài đặt virtualenvwrapper

    pip install virtualenvwrapper
    

    Activate virtualenv mỗi khi bật khởi động Terminal

    Các bạn thêm đoạn sau vào ~/.zshrc để virutalenv có thể dượcd khởi động mỗi khi bạn bật Terminal

    export WORKON_HOME=$HOME/.virtualenvs
    source /usr/local/bin/virtualenvwrapper.sh
    

    Tiến hành tạo môi trưởng ảo và cài đặt packages mong muốn

    • Tạo môi trường ảo
    hieunv@HieuNV ~ % mkvirtualenv a
    created virtual environment CPython3.7.6.final.0-64 in 515ms
      creator CPython3Posix(dest=/Users/hieunv/.virtualenvs/a, clear=False, global=False)
      seeder FromAppData(download=False, pip=latest, setuptools=latest, wheel=latest, via=copy, app_data_dir=/Users/hieunv/Library/Application Support/virtualenv/seed-app-data/v1)
      activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator
    virtualenvwrapper.user_scripts creating /Users/hieunv/.virtualenvs/a/bin/predeactivate
    virtualenvwrapper.user_scripts creating /Users/hieunv/.virtualenvs/a/bin/postdeactivate
    virtualenvwrapper.user_scripts creating /Users/hieunv/.virtualenvs/a/bin/preactivate
    virtualenvwrapper.user_scripts creating /Users/hieunv/.virtualenvs/a/bin/postactivate
    virtualenvwrapper.user_scripts creating /Users/hieunv/.virtualenvs/a/bin/get_env_details
    (a) hieunv@HieuNV ~ %
    

    Các bạn để ý dòng cuối cùng (a). Sau khi tạo xong thì zsh đã activate vào môi trường ảo.

    • Active vào môi trường ảo nếu môi trường chưa được active thì có thể làm như sau:
    hieunv@HieuNV ~ % workon a
    (a) hieunv@HieuNV ~ %
    
    • Khi muốn thoát khỏi môi trường ảo thì có thể làm như sau:
    (a) hieunv@HieuNV ~ % deactivate
    hieunv@HieuNV ~ %
    
    • Để cài đặt packages thì tiến hành cài đặt bằng pip như bình thường
    pip install boto3
    

    Sử dụng môi trường ảo với python 2

    • Cài đặt python 2
    brew install python2
    
    • Tạo môi trường ảo sử dụng python bằng tham số -p
    mkvirtualenv py2 -p python2
    

    Export package list để install trên máy khác

    (a) hieunv@HieuNV ~ % pip freeze > requirements.txt
    (a) hieunv@HieuNV ~ % cat requirements.txt
    boto3==1.12.11
    botocore==1.15.11
    docutils==0.15.2
    jmespath==0.9.5
    python-dateutil==2.8.1
    s3transfer==0.3.3
    six==1.14.0
    urllib3==1.25.8
    

    Cài đặt packages sử dụng requirements.txt

    pip install -r requirements.txt
    

    Tài liệu tham khảo:

    • https://swapps.com/blog/how-to-configure-virtualenvwrapper-with-python3-in-osx-mojave/