Category: Python

  • When it comes to compensation and benefits, other than base salary, which is the most important to you?

    When it comes to compensation and benefits, other than base salary, which is the most important to you?

    When it comes to compensation and benefits, other than base salary, which is the most important to you?

    Pic5

    This blog post is part of Udacity Data Scientists Nanodegree Program. Detailed analysis with all required code is posted in my github repository and Jupyter notebook.

    My github repository:

    My github repository

    Jupyter notebook code:

    My Jupyter notebook code

    Data source:

    Data source

    Each stage of the CRISP-DM process. These stages are:

    1. Business Understanding
    2. Data Understanding
    3. Prepare Data
    4. Data Modeling
    5. Evaluate the Resultssults

    Business Understanding

    In this Notebook, I will explore the 2023 Stack Overflow results to gather some information about the organization, main field of study, important benefits, and the increase in salary with the years of learning how to program.

    There are 4 questions I want to find answers to:

    • Question 1: What type of company or organization do you work for?
    • Question 2: What is your main field of study in college or university/for your undergraduate studies in 2023?
    • Question 3: Which is the most important benefits to you?
    • Question 4: The more you learn, will the more salary you get?

    Data Understanding

    In order to understanding data. I will do these steps:

    • Handle categorical data
    • Analyze, Model, and Visualize

    Question 1: What type of company or organization do you work for?

    pic1

    Evaluate the Results

    Through the chart, we can easily see:

    • The number of people working in Privately-held limited companies, not in startup mode, accounts for the largest number, about more than 16,000 people.
    • The number of people working in Publicly-traded corporations accounts for the second largest number, about 6,000 people.
    • The number of people working in State-owned companies is small, about less than 1,000 people. Thus, the difference between the number of people working between a Privately-held limited company and a State-owned company is quite large, about 15,000 people

    Question 2: What is your main field of study in college or university/for your undergraduate studies in 2023?

    pic2

    Evaluate the Results

    Through the chart, we can easily see:

    • The proportion of people graduating from field Computer science or software engineering is the largest, about 43%.
    • The rate of people graduating from field Computer engineering or electrical/electronics engineering ranks second, about 9%.
    • The rate of people graduating from field A health science is the least, about 1%. Thus, the proportion of people graduating from field Computer science or software engineering is much larger than field A health science, about 42%. Thereby, we see that field Computer science or software engineering is a field that is very attractive to learnersers.

    Question 3: Which is most important benefits to you?

    pic3

    Evaluate the Results

    Based on the chart, we see:

    • The benefit users are most interested in is Vacation/days off, accounting for about 15%.
    • Remote options is the second most important benefit, accounting for about 13.7%.
    • Child/elder care and Other are benefits that receive little attention, accounting for about 2% and 1% respectively. Lastly, when it comes to compensation and benefits, other than base salary, Vacation/days off is the following are most important to users

    Question 4: The more you learn, will the more salary you get?

    pic4

    Evaluate the Results

    Based on the map, we can see:

    • We can achieve the highest salary when we have 20 years or more of study experience.
    • From 19 to 20 years of study, we can reach the 2nd highest average salary.
    • The average salary is lowest when we have less than 1 year of study experience. From there we conclude that the more years of experience accumulated working on a project, the higher the average salary and vice versa.

    Conclusion

    In this article, we took a look at main field of study, important benefits, and the increase in salary with the years of learning how to program according to Stack Overflow 2023 survey data.

    We gathered the advice of the masses as to how to increase in salary with the years, which showed that the more years of experience accumulated working on a project, the higher the average salary and vice versa.

    When it comes to compensation and benefits, other than base salary, we then looked at the satisfaction of important benefits. This showed that Vacation/days off is the following are most important to users. We found that field Computer science or software engineering is a field that is very attractive to learnersers.

    The findings here are observational, not the result of a formal study

    To see more about this analysis, see the link to my Github available here

    View a detailed analysis report on Medium

    Blog_Medium

    Files Description in the repo

    The Stack Overflow Survey – 2023.ipynb – Notebook containing the data Analysis

    stack-overflow-developer-survey-2023.zip – Developers Survey Result Data and Result Schema

    Requirements

    pandas, matplotlib, jupyter-notebook (if running locally)

    License

    License

    Acknowledgements

    Data source:

    Data source

  • 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]

  • Neighborhood-Based Collaborative Filtering – Recommendation Systems – Phần 3

    Neighborhood-Based Collaborative Filtering – Recommendation Systems – Phần 3

    Neighborhood-Based Collaborative Filtering – Recommendation Systems – Phần 3

    Xin chào mọi người, ở bài viết trước thì mình đã giới thiệu qua về phương pháp User-based và item-based trong CF , ở bài viết lần này mình sẽ triển khai code Python để anh em có cái nhìn rõ hơn nhé.

    Nội dung trong phần này

    Giới thiệu

    Dữ liệu kiểm thử lần này mình sẽ lấy bộ dữ liệu MovieLens 100k được công bố năm 1998 bởi GroupLens. Bộ cơ sở dữ liệu này bao gồm 100,000 (100k) ratings từ 943 users cho 1682 bộ phim. Các bạn cũng có thể tìm thấy các bộ cơ sở dữ liệu tương tự với khoảng 1M, 10M, 20M ratings. Trong bài viết này, tôi sử dụng bộ cơ sở dữ liệu nhỏ nhất này nhằm mục đích minh hoạ.

    Sau khi download và giải nén, chúng ta sẽ thu được rất nhiều các file nhỏ, chúng ta chỉ cần quan tâm các file sau:

    • u.data : Chứa toàn bộ các ratings của 943 users cho 1682 movies. Mỗi user rate ít nhất 20 movies. Thông tin về thời gian rate cũng được cho nhưng chúng ta không sử dụng trong bài viết này.
    • ua.base, ua.test, ub.base, ub.test: là hai cách chia toàn bộ dữ liệu ra thành hai tập con, một cho training, một cho test. Chúng ta sẽ thực hành trên ua.base và ua.test. Bạn đọc có thể thử với cách chia dữ liệu còn lại.
    • u.user: Chứa thông tin về users, bao gồm: id, tuổi, giới tính, nghề nghiệp, zipcode (vùng miền), vì những thông tin này cũng có thể ảnh hưởng tới sở thích của các users. Tuy nhiên, trong bài viết này, chúng ta sẽ không sử dụng các thông tin này, trừ thông tin về id để xác định các user khác nhau.

    Phương pháp đánh giá

    Để đánh giá mô hình thì chúng ta sẽ sử dụng hai giá trị đó là :

    • Mean Absolute Error (MAE) :độ đo để đánh giá các mô hình hồi quy. MAE được định nghĩa là trung bình tổng trị tuyệt đối sai số giữa đầu ra dự đoán và kết quả thực . Công thức là :

    MAE

    • Root Mean Squared Error (RMSE):độ đo để đánh giá các mô hình hồi quy. RMSE được định nghĩa là căn bậc 2 trung bình tổng bình phương sai số giữa đầu ra dự đoán và kết quả thực:

    RMSE

    Code python

    import pandas as pd 
    import numpy as np
    from sklearn.metrics.pairwise import cosine_similarity
    from scipy import sparse 
    from numpy.linalg import matrix_power
    
    class CF(object):
       
       #Khai báo cho dữ liệu đầu vào là ma trận tiện ích , K item , hàm tính similarity , Tag User-based hoặc Item-Based 
       def __init__(self, Y_data, k, dist_func = cosine_similarity, uuCF = 1):
           # quy ước user-based (1) và  item-based (0) CF
           self.uuCF = uuCF 
           self.Y_data = Y_data if uuCF else Y_data[:, [1, 0, 2]]
           self.k = k
           self.dist_func = dist_func
           self.Ybar_data = None
           # Số lượng User và item . Vì id của user hoặc item được đếm từ 0 nên + thêm 1
           self.n_users = int(np.max(self.Y_data[:, 0])) + 1 
           self.n_items = int(np.max(self.Y_data[:, 1])) + 1
       
       
    
      
       def normalize_Y(self):
           # Tất cả các user và item 
           users = self.Y_data[:, 0]
           items = self.Y_data[:,1]
           
           # Tạo một ma trận để biến đổi giữ lại ma trận ban đầu để so sánh khi tính RMSE
           self.Ybar_data = self.Y_data.copy()
           self.mu = np.zeros((self.n_users,))
           for n in range(self.n_users):
               # Lấy id của User đã ratings
               # Convert lại thành kiểu Integer
               ids = np.where(users == n)[0].astype(np.int32)
               # Lấy id item được User có id ở trên rated
               item_ids = self.Y_data[ids, 1] 
               # Lấy giá trị ratings 
               ratings = self.Y_data[ids, 2]
               # Tính giá trị trung bình
               m = np.mean(ratings) 
               if np.isnan(m):
                   m = 0 # nếu ratings không có giá trị set = 0
               self.mu[n] = m
               # chuẩn hóa lại ma trận 
               self.Ybar_data[ids, 2] = ratings - self.mu[n]
    
           
           # Để tránh không đủ bộ nhớ khi số lượng người dùng và item quá lớn thì chúng ta chỉ lưu vị trí của các ratings khác 0
           self.Ybar = sparse.coo_matrix((self.Ybar_data[:, 2],
               (self.Ybar_data[:, 1], self.Ybar_data[:, 0])), (self.n_items, self.n_users))
           self.Ybar = self.Ybar.tocsr()
           
       
    
       # Hàm tính toán độ tương đồng giữa các User hoặc Item , ở đây tôi dùng thư viện cosine_similarity
       def similarity(self):
           eps = 1e-6
           self.S = self.dist_func(self.Ybar.T, self.Ybar.T)
       
           
       # Chuẩn hóa lại data và tính toán ma trận Similarity sau khi Đã điền ratings của các item chưa được rate
       def refresh(self):
           
           self.normalize_Y()
           self.similarity() 
           
       def fit(self):
           self.refresh()
           
       
       
       # Chuẩn đoán rating của user u for item i 
       def __pred(self, u, i, normalized = 1):
           
           
           # Step 1: Tìm tất cả user đã rated cho item i
           ids = np.where(self.Y_data[:, 1] == i)[0].astype(np.int32)
           # Step 2: 
           users_rated_i = (self.Y_data[ids, 0]).astype(np.int32)
           # Step 3: Tìm sự tương đồng giữa User n với các User còn lại
           sim = self.S[u, users_rated_i]
           # Step 4: Tìm K User có sự tương đồng nhất
           a = np.argsort(sim)[-self.k:] 
           nearest_s = sim[a]
           
           r = self.Ybar[i, users_rated_i[a]]
           if normalized:
               # Thêm 1 giá trị nhỏ để tránh chia cho 0
               return (r*nearest_s)[0]/(np.abs(nearest_s).sum() + 1e-8)
    
           return (r*nearest_s)[0]/(np.abs(nearest_s).sum() + 1e-8) + self.mu[u]
       
       def pred(self, u, i, normalized = 1):
           
           # Dự đoán ratings user u cho item i
           if self.uuCF: return self.__pred(u, i, normalized)
           return self.__pred(i, u, normalized)
               
       
       def recommend(self, u):
           
           # Đề xuất item cho User u với ratings > 0 
           ids = np.where(self.Y_data[:, 0] == u)[0]
           items_rated_by_u = self.Y_data[ids, 1].tolist()              
           recommended_items = []
           for i in range(self.n_items):
               if i not in items_rated_by_u:
                   rating = self.__pred(u, i)
                   if rating > 0: 
                       recommended_items.append(i)
           
           return recommended_items 
       
       def recommend2(self, u):
          # Để xuất User cho item u với ratings > 0 
           ids = np.where(self.Y_data[:, 0] == u)[0]
           items_rated_by_u = self.Y_data[ids, 1].tolist()              
           recommended_items = []
       
           for i in range(self.n_items):
               if i not in items_rated_by_u:
                   rating = self.__pred(u, i)
                   if rating > 0: 
                       recommended_items.append(i)
           
           return recommended_items 
    
       def print_recommendation(self):
           
           # Print ra đề xuất
           print ('Recommendation: ')
           for u in range(self.n_users):
               recommended_items = self.recommend(u)
               if self.uuCF:
                   print ('Recommend item(s):', recommended_items , 'for user', u)
             
               else: 
                   print ('Recommend item', u , 'for user(s) : ', recommended_items)
               
    

    So sánh

    User-based

    rs = CF(rate_train, k = 30, uuCF = 0)
    rs.fit()
    
    n_tests = rate_test.shape[0]
    SE = 0 # squared error
    AE = 0
    for n in range(n_tests):
        pred = rs.pred(rate_test[n, 0], rate_test[n, 1], normalized = 0)
        SE += (pred - rate_test[n, 2])**2
        AE += (pred - rate_test[n,2])
    
    
    RMSE = np.sqrt(SE/n_tests)
    MAE = abs(AE/n_tests)
    print (' RMSE =', RMSE)
    print (' MAE =', MAE)
    

    Kết quả : RMSE = 0.9867912132705384 và MAE = 0.0582653956596381

    Có thể thấy MAE và RMSE rất nhỏ cho thấy hệ thống chạy khá ok

    Item-based

    rs = CF(rate_train, k = 30, uuCF = 1)
    rs.fit()
    
    n_tests = rate_test.shape[0]
    SE = 0 # squared error
    AE = 0
    for n in range(n_tests):
       pred = rs.pred(rate_test[n, 0], rate_test[n, 1], normalized = 0)
       SE += (pred - rate_test[n, 2])**2
       AE += (pred - rate_test[n,2])
       
    MAE = abs(AE/n_tests)
    RMSE = np.sqrt(SE/n_tests)
    print (' RMSE =', RMSE)
    print (' MAE =', MAE)
    

    Kết quả : RMSE = 0.9951981100882598 và MAE = 0.03062686975303437

    Kết quả cũng rất ấn tượng khi MAE còn thấp hơn cả phương pháp User-based nhưng về cơ bản không có sự cách biệt quá lớn, các bạn cũng có thể thay hệ số K lớn hơn để thấy rõ được sự khác biệt về kết quả . Thực ra kết quả này bị ảnh hướng rất nhiều từ file data , đôi khi dữ liệu về item lại cực lớn so với user thì phương pháp item based lại tỏ ra mạnh mẽ hơn và ngược lại . Thực tế thì hai cách này luôn đi song song với nhau để đưa ra tư vấn cho người dùng.

    Mình cũng xin kết thúc bài viết ở đây , ở bài tiếp theo mình sẽ đề cập đến phương kết hợp giữa cả user và item .

    Tài liệu mình hay tham khảo

    1. Khá hay và nhiều kiến thức của các pháp sư US-UK AWS Machine Learning Blog

    2. Kho Dataset cho anh em GroupLens

    3. Mình cũng hay tham khảo ở đây TensorFlowBlog

  • 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/

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

    Giới thiệu về Transfer learning

    Mở đầu

    Trong quá trình xây dựng một mô hình học máy, chắc hẳn các bạn đã gặp phải một số vấn đề như mô hình dự đoán có độ chính xác thấp dù đã dùng một kiến trúc phức tạp, hay lượng dữ liệu quá ít để có thể huấn luyện một mô hình hoàn chỉnh. Thông thường, một mô hình có kết quả dự báo kém là do một số nguyên nhân sau

    • Dữ liệu nhỏ không đại diện: Bộ dữ liệu của chúng ta có kích thước quá bé so với mô hình được huấn luyện, khiến cho mô hình không học được các đặc trưng của ảnh để giải quyết các bài toán cụ thể như phân loại, nhận dạng, …

    • Mô hình mất cân bằng dữ liệu: Khi mà tập dữ liệu của chúng ta có sự chênh lệch lớn giữa các nhóm, ví dụ như chỉ có 100 ảnh cho con chó và 100.000 ảnh cho con mèo, tất nhiên mô hình sẽ “thiên vị” dự đoán nghiêng về con mèo nhiều hơn

    • Kiến trúc mô hình quá phức tạp: Khi ta có bộ dữ liệu lớn tới vài trăm triệu ảnh thì tất nhiên kiến trúc mô hình phức tạp có thể mang lại độ chính xác cao. Tuy nhiên đối với những bộ dữ liệu nhỏ, vừa phải thì mô hình quá phức tạp lại đem lại độ chính xác không cao. Cần chọn kiến trúc mô hình phù hợp với lượng data chúng ta làm việc cùng

    • Quá trình tối ưu hóa gặp khó khăn: Có thể các hyperparameter được thiết lập chưa tốt như learning rate khiến cho mô hình huấn luyện lâu hội tụ hoặc chưa đạt tới điểm global optimal

    Vậy khi chúng ta có một lượng data nhỏ nhưng muốn hoàn thiện một bài toán sử dụng mô hình hoàn chỉnh, làm thế nào để mô hình đó hoạt động tốt? Transfer Learning có thế giải quyết điều đó.

    Introduction

    Transfer learning – Học chuyển giao bao gồm việc sử dụng các tính năng đã học về một vấn đề và tận dụng chúng vào một vấn đề mới nhưng tương tự. Ví dụ, các tính năng từ một mô hình đã học để xác định các loài chim có thể hữu ích để khởi động một mô hình dùng để xác định tanukis(1 loài lửng của Nhật Bản).

    Học chuyển giao thường được thực hiện cho các nhiệm vụ mà tập dữ liệu của bạn có quá ít dữ liệu để đào tạo một mô hình quy mô đầy đủ từ đầu.

    Hiện thân phổ biến nhất của học chuyển tiếp trong học sâu là quy trình làm việc sau:

    • Lấy các lớp từ một mô hình đã được đào tạo trước đó.
    • Đóng băng(Freeze) chúng, để tránh phá hủy bất kỳ thông tin nào mà chúng chứa trong các đợt huấn luyện trong tương lai.
    • Thêm một số lớp mới, có thể đào tạo lên trên các lớp đã đóng băng. Chúng sẽ học cách biến các tính năng cũ thành dự đoán trên một tập dữ liệu mới.
    • Đào tạo các lớp mới trên tập dữ liệu của bạn.

    Bước cuối cùng (tùy chọn), là tinh chỉnh, bao gồm việc mở toàn bộ mô hình bạn đã thu được ở trên (hoặc một phần của nó) và đào tạo lại nó trên dữ liệu mới với tỷ lệ học tập rất thấp. Điều này có thể đạt được những cải tiến có ý nghĩa, bằng cách từng bước điều chỉnh các tính năng được đào tạo trước cho phù hợp với dữ liệu mới.

    Đầu tiên, chúng ta sẽ xem xét chi tiết về API có thể train của Keras, làm cơ sở cho hầu hết các quy trình học tập và tinh chỉnh chuyển giao.

    Sau đó, chúng tôi sẽ chứng minh quy trình làm việc điển hình bằng cách lấy một mô hình được đào tạo trước trên tập dữ liệu ImageNet và đào tạo lại nó trên tập dữ liệu phân loại Kaggle "mèo vs chó".

    Điều này được điều chỉnh từ Deep Learning với Python và bài đăng trên blog năm 2016 "xây dựng các mô hình phân loại hình ảnh mạnh mẽ bằng cách sử dụng rất ít dữ liệu".

    Các lớp đóng băng: hiểu thuộc tính trainable (có thể đào tạo)

    Các lớp và mô hình có ba thuộc tính trọng số:

    • weight (trọng số) : danh sách tất cả các biến trọng số của lớp.
    • trainable_weights (trọng số có thể đào tạo) : danh sách những thứ cần được cập nhật (thông qua gradient descent) để giảm thiểu tổn thất trong quá trình đào tạo.
    • non_trainable_weights (trọng số không thể đào tạo): là danh sách những thứ không được đào tạo. Thông thường, chúng được cập nhật bởi mô hình trong quá trình chuyển tiếp.

    Ví dụ : lớp Dense có 2 trainable_weights (kernel & bias)

    layer = keras.layers.Dense(3)
    layer.build((None, 4))  # Khởi tạo trọng số
    
    print("weights:", len(layer.weights))
    print("trainable_weights:", len(layer.trainable_weights))
    print("non_trainable_weights:", len(layer.non_trainable_weights))
    
    weights: 2
    trainable_weights: 2
    non_trainable_weights: 0
    

    Nói chung, tất cả các trọng số đều có thể huấn luyện được. Lớp tích hợp duy nhất có trọng số không thể đào tạo là lớp BatchNormalization. Nó sử dụng trọng số không thể đào tạo để theo dõi giá trị trung bình và phương sai của các đầu vào trong quá trình đào tạo. Để tìm hiểu cách sử dụng trọng số không thể đào tạo trong các lớp tùy chỉnh của riêng bạn, hãy xem hướng dẫn viết các lớp mới từ đầu.

    Ví dụ: lớp BatchNormalization có 2 trainable_weights và 2 non_trainable_weights

    layer = keras.layers.BatchNormalization()
    layer.build((None, 4))  # Khởi tạo trọng số
    
    print("weights:", len(layer.weights))
    print("trainable_weights:", len(layer.trainable_weights))
    print("non_trainable_weights:", len(layer.non_trainable_weights))
    
    weights: 4
    trainable_weights: 2
    non_trainable_weights: 2
    

    Các lớp và mô hình cũng có thuộc tính boolean có thể đào tạo. Giá trị của nó có thể được thay đổi. Đặt layer.trainable thành False sẽ di chuyển tất cả trọng lượng của lớp từ có thể huấn luyện sang không thể huấn luyện. Đây được gọi là "đóng băng" lớp: trạng thái của lớp bị đóng băng sẽ không được cập nhật trong quá trình đào tạo (khi đào tạo với fit() hoặc khi đào tạo với bất kỳ vòng lặp tùy chỉnh nào dựa vào trainable_weights để áp dụng cập nhật gradient).

    Ví dụ: đặt trainable thành False

    layer = keras.layers.Dense(3)
    layer.build((None, 4))  # Khởi tạo trọng số
    layer.trainable = False  # Đóng băng layer
    
    print("weights:", len(layer.weights))
    print("trainable_weights:", len(layer.trainable_weights))
    print("non_trainable_weights:", len(layer.non_trainable_weights))
    
    weights: 2
    trainable_weights: 0
    non_trainable_weights: 2
    

    Khi trọng số trainable trở thành non-trainable, giá trị của nó sẽ không còn được cập nhật trong quá trình huấn luyện.

    # Tạo mô hình với 2 layer
    layer1 = keras.layers.Dense(3, activation="relu")
    layer2 = keras.layers.Dense(3, activation="sigmoid")
    model = keras.Sequential([keras.Input(shape=(3,)), layer1, layer2])
    
    # Đóng băng layer1
    layer1.trainable = False
    
    # Giữ lại một bản sao trọng số của layer1 để tham khảo sau này
    initial_layer1_weights_values = layer1.get_weights()
    
    # Huấn luyện mô hình
    model.compile(optimizer="adam", loss="mse")
    model.fit(np.random.random((2, 3)), np.random.random((2, 3)))
    
    # Kiểm tra trọng số của layer1 không hề thay đổi trong quá trình training
    final_layer1_weights_values = layer1.get_weights()
    np.testing.assert_allclose(
        initial_layer1_weights_values[0], final_layer1_weights_values[0]
    )
    np.testing.assert_allclose(
        initial_layer1_weights_values[1], final_layer1_weights_values[1]
    )
    
    1/1 [==============================] - 0s 333ms/step - loss: 0.1007
    

    Cài đặt đệ quy của thuộc tính trainable

    Nếu bạn đặt trainable = False trong một model hoặc một lớp có nhiều lớp con, tất cả lớp con sẽ trở thành non-trainable Ví dụ :

    inner_model = keras.Sequential(
        [
            keras.Input(shape=(3,)),
            keras.layers.Dense(3, activation="relu"),
            keras.layers.Dense(3, activation="relu"),
        ]
    )
    
    model = keras.Sequential(
        [keras.Input(shape=(3,)), inner_model, keras.layers.Dense(3, activation="sigmoid"),]
    )
    
    model.trainable = False  # Đóng băng model 
    
    assert inner_model.trainable == False  # Tất cả lớp thuộc `model` dều bị đóng băng
    assert inner_model.layers[0].trainable == False
    

    Transfer learning workflow

    Một workflow điển hình của transfer learning được thực thi trong Keras:

    1. Khởi tạo mô hình cơ sở và tải các trọng số đã được đào tạo trước vào đó.
    2. Cố định tất cả các lớp trong mô hình cơ sở bằng cách đặt trainable = False.
    3. Tạo một mô hình mới trên đầu ra của một (hoặc một số) lớp từ mô hình cơ sở.
    4. Train mô hình mới trên tập dữ liệu mới của bạn.

    Lưu ý rằng có thể là một quy trình thay thế nhẹ hơn :

    1. Khởi tạo mô hình cơ sở và tải các trọng số đã được đào tạo trước vào đó.
    2. Chạy tập dữ liệu mới của bạn thông qua nó và ghi lại kết quả của một (hoặc một số) lớp từ mô hình cơ sở. Đây được gọi là feature extraction.
    3. Sử dụng đầu ra đó làm dữ liệu đầu vào cho một mô hình mới nhỏ hơn.

    Một ưu điểm chính của quy trình làm việc thứ hai đó là bạn chỉ chạy mô hình cơ sở một lần trên dữ liệu của mình, thay vì một lần cho mỗi epoch đào tạo. Vì vậy, nó nhanh hơn và tiết kiệm tài nguyên hơn rất nhiều.

    Tuy nhiên, một vấn đề với quy trình làm việc thứ hai đó là nó không cho phép bạn tự động sửa đổi dữ liệu đầu vào của mô hình mới trong quá trình đào tạo, ví dụ như bắt buộc khi thực hiện tăng dữ liệu. Học chuyển giao thường được sử dụng cho các nhiệm vụ khi tập dữ liệu mới của bạn có quá ít dữ liệu để đào tạo mô hình quy mô đầy đủ từ đầu và trong các tình huống như vậy, việc tăng dữ liệu là rất quan trọng. Vì vậy, trong những gì tiếp theo, chúng ta sẽ tập trung vào quy trình làm việc đầu tiên.

    Đây là quy trình làm việc đầu tiên trong Keras:

    Đầu tiên, khởi tạo một mô hình cơ sở với các trọng lượng được đào tạo trước.

    base_model = keras.applications.Xception(
        weights='imagenet',  # Load trọng số pre-trained tại ImageNet.
        input_shape=(150, 150, 3),
        include_top=False)  # Không bao gồm lớp phân loại ImageNet ở trên cùng
    

    Sau đó, đóng băng mô hình cơ sở

    base_model.trainable = False
    

    Tạo một mô hình mới trên đầu trang.

    inputs = keras.Input(shape=(150, 150, 3))
    # Đảm bảo base_model đang chạy chế độ inference ở đây,
    # bằng cách sử dụng `training=False`. Điều này rất quan trọng cho fine-tuning, 
    # mà tôi sẽ nhắc tới ở phần sau
    x = base_model(inputs, training=False)
    # Chuyển đổi các thuộc tính cấu hình của `base_model.output_shape[1:]` thành vectors
    x = keras.layers.GlobalAveragePooling2D()(x)
    # Một bộ phân loại Dense với một đơn vị duy nhất (phân loại nhị phân)
    outputs = keras.layers.Dense(1)(x)
    model = keras.Model(inputs, outputs)
    

    Train model với dữ liệu mới

    model.compile(optimizer=keras.optimizers.Adam(),
                  loss=keras.losses.BinaryCrossentropy(from_logits=True),
                  metrics=[keras.metrics.BinaryAccuracy()])
    model.fit(new_dataset, epochs=20, callbacks=..., validation_data=...)
    

    Kết luận

    Trên đây là tổng quan về Transfer Learning. Ở bài viết tiếp theo chúng ta sẽ tìm hiểu về Fine-tuning, một kỹ thuật Transfer Leaning.

    Thanks for reading

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

  • Tổng quan thư viện Underthesea – bộ công cụ mã nguồn mở xử lý ngôn ngữ tự nhiên tiếng Việt

    Tổng quan thư viện Underthesea – bộ công cụ mã nguồn mở xử lý ngôn ngữ tự nhiên tiếng Việt

    Giới thiệu thư viện Underthesea – bộ công cụ mã nguồn mở xử lý ngôn ngữ tự nhiên tiếng Việt

    Mở đầu

    Underthesea là một toolkit hỗ trợ cho việc nghiên cứu và phát triển xử lý ngôn ngữ tự nhiên tiếng Việt. Underthesea ra đời vào tháng 3 năm 2017, trong bối cảnh ở Việt Nam đã có một số toolkit khá tốt như vn.vitk, pyvi, nhưng vẫn thiếu một toolkit hoàn chỉnh, mã nguồn mở, dễ dàng cài đặt và sử dụng như các sản phẩm tương đương đối với tiếng Anh như nltk, polyglot, spacy.

    Trong bài viết này chúng ta sẽ tìm hiểu sơ qua về Underthesea và một số cách sử dụng của nó.

    Underthesea là :

    1. Một bộ công cụ NLP tiếng Việt <br> Underthesea là một mã nguồn mở bằng Python bao gồm các bộ dữ liệu (data sets) và các hướng dẫn hỗ trợ nghiên cứu và phát triển trong xử lý ngôn ngữ tự nhiên tiếng Việt (Vietnamese Natural Language Processing). Nó cung cấp các API cực kỳ dễ dàng để áp dụng các mô hình pretrained NLP cho văn bản tiếng Việt, chẳng hạn như phân đoạn từ, gắn thẻ một phần giọng nói(PoS), nhận dạng thực thể có tên (NER), phân loại văn bản và phân tích cú pháp phụ thuộc.

    2. Một thư viện Pytorch <br> Underthesea được hỗ trợ bởi một trong những thư viên học sâu phổ biến nhất, Pytorch, giúp nó dễ dàng train các mô hình học sâu và thử nghiệp các phương pháp tiếp cận mới bằng cách sử dụng các Module và Class của Underthesea

    3. Một phần mềm mã nguồn mở <br> Underthesea được công bố theo giấy phép GNU General Public License v3.0. Các quyền của giấy phép này có điều kiện là cung cấp mã nguồn hoàn chỉnh của các tác phẩm được cấp phép và sửa đổi, bao gồm các tác phẩm lớn hơn sử dụng tác phẩm được cấp phép, theo cùng một giấy phép.

    Cài đặt

    Để cài đặt underthesea :

    $ pip install underthesea
    

    Hướng dẫn

    1. Phân đoạn câu (Sentence Segmentation)
    >>> from underthesea import sent_tokenize
    >>> text = 'Taylor cho biết lúc đầu cô cảm thấy ngại với cô bạn thân Amanda nhưng rồi mọi thứ trôi qua nhanh chóng. Amanda cũng thoải mái với mối quan hệ này.'
    
    >>> sent_tokenize(text)
    [
      "Taylor cho biết lúc đầu cô cảm thấy ngại với cô bạn thân Amanda nhưng rồi mọi thứ trôi qua nhanh chóng.",
      "Amanda cũng thoải mái với mối quan hệ này."
    ]
    
    1. Phân đoạn từ (Word Segmentation)
    >>> from underthesea import word_tokenize
    >>> sentence = 'Chàng trai 9X Quảng Trị khởi nghiệp từ nấm sò'
    
    >>> word_tokenize(sentence)
    ['Chàng trai', '9X', 'Quảng Trị', 'khởi nghiệp', 'từ', 'nấm', 'sò']
    
    >>> word_tokenize(sentence, format="text")
    'Chàng_trai 9X Quảng_Trị khởi_nghiệp từ nấm sò'
    
    1. Gán nhãn POS
    >>> from underthesea import pos_tag
    >>> pos_tag('Chợ thịt chó nổi tiếng ở Sài Gòn bị truy quét')
    [('Chợ', 'N'),
     ('thịt', 'N'),
     ('chó', 'N'),
     ('nổi tiếng', 'A'),
     ('ở', 'E'),
     ('Sài Gòn', 'Np'),
     ('bị', 'V'),
     ('truy quét', 'V')]
    
    1. Chunking
    >>> text = 'Bác sĩ bây giờ có thể thản nhiên báo tin bệnh nhân bị ung thư?'
    >>> chunk(text)
    [('Bác sĩ', 'N', 'B-NP'),
     ('bây giờ', 'P', 'I-NP'),
     ('có thể', 'R', 'B-VP'),
     ('thản nhiên', 'V', 'I-VP'),
     ('báo tin', 'N', 'B-NP'),
     ('bệnh nhân', 'N', 'I-NP'),
     ('bị', 'V', 'B-VP'),
     ('ung thư', 'N', 'I-VP'),
     ('?', 'CH', 'O')]
    
    1. Phân tích cú pháp phụ thuộc
    >>> from underthesea import dependency_parse
    >>> text = 'Tối 29/11, Việt Nam thêm 2 ca mắc Covid-19'
    >>> dependency_parse(text)
    [('Tối', 5, 'obl:tmod'),
     ('29/11', 1, 'flat:date'),
     (',', 1, 'punct'),
     ('Việt Nam', 5, 'nsubj'),
     ('thêm', 0, 'root'),
     ('2', 7, 'nummod'),
     ('ca', 5, 'obj'),
     ('mắc', 7, 'nmod'),
     ('Covid-19', 8, 'nummod')]
    
    1. Gán nhãn thực thể có tên (Named Entity Recognition)
    >>> from underthesea import ner
    >>> text = 'Chưa tiết lộ lịch trình tới Việt Nam của Tổng thống Mỹ Donald Trump'
    >>> ner(text)
    [('Chưa', 'R', 'O', 'O'),
     ('tiết lộ', 'V', 'B-VP', 'O'),
     ('lịch trình', 'V', 'B-VP', 'O'),
     ('tới', 'E', 'B-PP', 'O'),
     ('Việt Nam', 'Np', 'B-NP', 'B-LOC'),
     ('của', 'E', 'B-PP', 'O'),
     ('Tổng thống', 'N', 'B-NP', 'O'),
     ('Mỹ', 'Np', 'B-NP', 'B-LOC'),
     ('Donald', 'Np', 'B-NP', 'B-PER'),
     ('Trump', 'Np', 'B-NP', 'I-PER')]
    
    1. Phân loại văn bản
    >>> from underthesea import classify
    
    >>> classify('HLV đầu tiên ở Premier League bị sa thải sau 4 vòng đấu')
    ['The thao']
    
    >>> classify('Hội đồng tư vấn kinh doanh Asean vinh danh giải thưởng quốc tế')
    ['Kinh doanh']
    
    >> classify('Lãi suất từ BIDV rất ưu đãi', domain='bank')
    ['INTEREST_RATE']
    
    1. Phân tích cảm xúc
    >>> from underthesea import sentiment
    
    >>> sentiment('hàng kém chất lg,chăn đắp lên dính lông lá khắp người. thất vọng')
    negative
    >>> sentiment('Sản phẩm hơi nhỏ so với tưởng tượng nhưng chất lượng tốt, đóng gói cẩn thận.')
    positive
    
    >>> sentiment('Đky qua đường link ở bài viết này từ thứ 6 mà giờ chưa thấy ai lhe hết', domain='bank')
    ['CUSTOMER_SUPPORT#negative']
    >>> sentiment('Xem lại vẫn thấy xúc động và tự hào về BIDV của mình', domain='bank')
    ['TRADEMARK#positive']
    
    1. Tài nguyên NLP tiếng Việt

    Danh sách tài nguyên

    $ underthesea list-data
    | Name                | Type        | License | Year | Directory                    |
    |---------------------+-------------+---------+------+------------------------------|
    | UIT_ABSA_RESTAURANT | Sentiment   | Open    | 2021 | datasets/UIT_ABSA_RESTAURANT |
    | UIT_ABSA_HOTEL      | Sentiment   | Open    | 2021 | datasets/UIT_ABSA_HOTEL      |
    | SE_Vietnamese-UBS   | Sentiment   | Open    | 2020 | datasets/SE_Vietnamese-UBS   |
    | CP_Vietnamese-UNC   | Plaintext   | Open    | 2020 | datasets/CP_Vietnamese-UNC   |
    | DI_Vietnamese-UVD   | Dictionary  | Open    | 2020 | datasets/DI_Vietnamese-UVD   |
    | UTS2017-BANK        | Categorized | Open    | 2017 | datasets/UTS2017-BANK        |
    | VNTQ_SMALL          | Plaintext   | Open    | 2012 | datasets/LTA                 |
    | VNTQ_BIG            | Plaintext   | Open    | 2012 | datasets/LTA                 |
    | VNESES              | Plaintext   | Open    | 2012 | datasets/LTA                 |
    | VNTC                | Categorized | Open    | 2007 | datasets/VNTC                |
    
    $ underthesea list-data --all
    

    Download tài nguyên

    $ underthesea download-data VNTC
    100%|██████████| 74846806/74846806 [00:09<00:00, 8243779.16B/s]
    Resource VNTC is downloaded in ~/.underthesea/datasets/VNTC folder
    

    Các tính năng sắp ra mắt

    • Dịch máy
    • Chuyển văn bản thành giọng nói
    • Nhận dạng giọng nói tự động

    Kết bài

    Với Underthesea, chúng ta có thể dễ dàng cài đặt, sử dụng và tiết kiệm được lượng lớn thời gian thay vì phải gán nhãn bằng tay. Underthesea cũng là thư viện đắc lực hỗ trợ xử lý dữ liệu đầu vào cho rất nhiều bài toán khác.

    Cảm ơn các bạn đã giành thời gian đọc.

    Tham khảo: https://pypi.org/project/underthesea/

  • 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…)

  • Upload file dung lượng lớn tới S3 với Multipart và Presign-url

    Upload file dung lượng lớn tới S3 với Multipart và Presign-url

    Nếu như bình thường ta thực hiện single upload tới s3 thì sẽ có 2 cách sau:

    • Upload file lên s3 qua server của chúng ta: Cái này thì reject vì chúng ta phải tốn thời gian upload lên server của mình rồi từ đó mới upload lên S3. Với large file thì gần như là không nên.

    • Upload từ client bằng cách sử dụng presign-url. Thay vì phải qua server trung gian thì chúng ta upload thẳng lên S3. Tốc độ cải thiện rất nhiều vì cách 1 đa phần thời gian tốn ở khâu upload lên server của chúng ta.

    Nhưng nói gì thì nói single upload to S3 thì sẽ có những hạn chế sau dưới Maximum size chỉ là 5GB . Điều này thực sự hạn chế. Hơn nữa AWS cũng suggest chúng ta là file >100MB thì nên sử dụng Multipart Upload . Vậy ưu điểm chính của nó là:

    • Maximum size là 5TB
    • Tốc độ upload tốt hơn

    Điều đó mình lựa chọn multipart và presign-url cho bài toán upload file dung lượng lớn. Server ở đây mình sử dụng là python. Client thì dùng angular.

    part1

    Server

    Phần server này mình sử dụng kiến trúc serverless để triển khai với ngôn ngữ Python Flask trên môi trường AWS Cloud

    Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

    Phần Server chủ yếu sẽ có 3 api chính:

    • Đầu tiên là api start-upload, với logic là request tới s3 để lấy uploadId

      @app.route("/start-upload", methods=["GET"])
      def start_upload():
          file_name = request.args.get('file_name')
          response = s3.create_multipart_upload(
              Bucket=BUCKET_NAME, 
              Key=file_name
          )
      
          return jsonify({
              'upload_id': response['UploadId']
          })
      
    • Tiếp theo là api get-upload-url để lấy presignurl cho từng part của file khi upload

      @app.route("/get-upload-url", methods=["GET"])
      def get_upload_url():
           file_name = request.args.get('file_name')
           upload_id = request.args.get('upload_id')
           part_no = request.args.get('part_no')
           signed_url = s3.generate_presigned_url(
               ClientMethod ='upload_part',
               Params = {
                   'Bucket': BUCKET_NAME,
                   'Key': file_name, 
                   'UploadId': upload_id, 
                   'PartNumber': int(part_no)
               }
           )
      
           return jsonify({
               'upload_signed_url': signed_url
           })
      
    • Cuối cùng đây là api để kiểm tra xem việc upload đã hoàn thành chưa.

       @app.route("/complete-upload", methods=["POST"])
       def complete_upload():
           file_name = request.json.get('file_name')
           upload_id = request.json.get('upload_id')
           print(request.json)
           parts = request.json.get('parts')
           response = s3.complete_multipart_upload(
               Bucket = BUCKET_NAME,
               Key = file_name,
               MultipartUpload = {'Parts': parts},
               UploadId= upload_id
           )
           
           return jsonify({
               'data': response
           })
      

    Client

    Phần client này mình dùng Angular để triển khai 1 trang web upload file đơn giản.

    Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

    Khi có action upload đầu tiên ta sẽ gọi tới function uploadMultipartFile. Function uploadMultipartFile có chức năng với các step sau:

    • Call api start-upload để lấy được uploadId.

      const uploadStartResponse = await this.startUpload({
          fileName: file.name,
          fileType: file.type
      });
      
    • Split file upload thành các chunks, ở đây chia thành 10MB/chunk nhé. Ở đây mình chia là 10MB vì minimum size của Multipart Upload là 10MB.

    • Thực hiện call api get-upload-url để lấy preSignurl cho từng chunk ở trên.

    • Upload các chunks bằng signurl.

    • Khi tất cả các chunks upload xong sẽ thực hiện call api complete-upload để xác nhận với S3 mình đã upload đầy đủ các part. Done.

      try {
          const FILE_CHUNK_SIZE = 10000000; // 10MB
          const fileSize = file.size;
          const NUM_CHUNKS = Math.floor(fileSize / FILE_CHUNK_SIZE) + 1;
          let start, end, blob;
      
          let uploadPartsArray = [];
          let countParts = 0;
      
          let orderData = [];
      
          for (let index = 1; index < NUM_CHUNKS + 1; index++) {
            start = (index - 1) * FILE_CHUNK_SIZE;
            end = (index) * FILE_CHUNK_SIZE;
            blob = (index < NUM_CHUNKS) ? file.slice(start, end) : file.slice(start);
      
            // (1) Generate presigned URL for each part
            const uploadUrlPresigned = await this.getPresignUrl({
              fileName: file.name,
              fileType: file.type,
              partNo: index.toString(),
              uploadId: uploadStartResponse.upload_id
            });
      
            // (2) Puts each file part into the storage server
      
            orderData.push({
              presignedUrl: uploadUrlPresigned.upload_signed_url,
              index: index
            });
      
            const req = new HttpRequest('PUT', uploadUrlPresigned.upload_signed_url, blob, {
              reportProgress: true
            });
      
            this.httpClient
              .request(req)
              .subscribe((event: HttpEvent<any>) => {
                switch (event.type) {
                  case HttpEventType.UploadProgress:
                    const percentDone = Math.round(100 * event.loaded / FILE_CHUNK_SIZE);
                    this.uploadProgress$.emit({
                      progress: file.size < FILE_CHUNK_SIZE ? 100 : percentDone,
                      token: tokenEmit
                    });
                    break;
                  case HttpEventType.Response:
                    console.log('Done!');
                }
      
                // (3) Calls the CompleteMultipartUpload endpoint in the backend server
      
                if (event instanceof HttpResponse) {
                  const currentPresigned = orderData.find(item => item.presignedUrl === event.url);
      
                  countParts++;
                  uploadPartsArray.push({
                    ETag: event.headers.get('ETag').replace(/[|&;$%@"<>()+,]/g, ''),
                    PartNumber: currentPresigned.index
                  });
                  if (uploadPartsArray.length === NUM_CHUNKS) {
                    console.log(file.name)
                    console.log(uploadPartsArray)
                    console.log(uploadStartResponse.upload_id)
                    this.httpClient.post(`${this.url}/complete-upload`, {
                      file_name: encodeURIComponent(file.name),
                      parts: uploadPartsArray.sort((a, b) => {
                        return a.PartNumber - b.PartNumber;
                      }),
                      upload_id: uploadStartResponse.upload_id
                    }).toPromise()
                      .then(res => {
                        this.finishedProgress$.emit({
                          data: res
                        });
                      });
                  }
                }
              });
          }
        } catch (e) {
          console.log('error: ', e);
        }
      

    Có những chú ý sau.

    • Minimum size của Multipart Upload là 10MB. Maximum là 5TB
    • Được upload tối đã 10000 chunks cho 1 file thôi. Ví dụ ở đây mình chia 1 chunk là 10MB thì mình chỉ upload tối đa 200GB. Tương tự 1 chunk là 5GB => upload tối đa là 5TB.

    Demo

    • Cài đặt server với serverless. Vào project serverless và gõ lệnh

      sls deploy
      

      Có những chú ý sau:

      • Sau khi cài đặt NodeJS thì cài Serverless framework

        npm i -g serverless
        
      • Cần có account AWS

      • Config ở file serverless đang sử dụng aws profile mà mình đã setup ở local(khanhcd92). Nếu bạn dùng default hoặc cấu hình profile riêng thì hãy thay đổi nó nhé. Cách cấu hình aws profile.

        provider:
          name: aws
          runtime: python3.6
          stage: dev
          region: ap-southeast-1
          profile: khanhcd92
        
    • Thay đường dẫn API endpoint từ output của cài đặt server cho url trong class UploadService của project web

    • Cài đặt thư viện trong project web

      npm i
      
    • Chạy web angular ở local với lệnh

      npm start
      

      part2

    • Kéo file cần upload vào trình duyệt. Chỗ này mình sẽ dùng 1 file có dụng lượng khoảng 75M để upload.

      part3

    • Kiểm tra kết quả trên S3 part4

    Chi tiết source code các bạn xem ở đây nhé.