diff --git a/_config.yml b/_config.yml index d13c687b5b6..ba7f6565b7d 100644 --- a/_config.yml +++ b/_config.yml @@ -1,6 +1,6 @@ -title: "Blog Title" -author: "Blog Author" -description: "Made with <3" +title: "Artificial Intelligence Blog" +author: "Viet Le Hoang" +description: "Ho Chi Minh University of Technology" permalink: /:title/ lang: "en" excerpt_separator: "\n\n\n" @@ -16,13 +16,13 @@ minimal: false # use a dark header # Menu navigation: # accepts {file, title, url, icon, sidebaricon} - - {file: "index.html"} - - {file: "README.md"} + - {file: "index.html", title: "Homepage"} + - {file: "about_me.md", title: "About"} external: # shows a footer with social links - for available icons see fontawesome.com/icons - - {title: Mail, icon: envelope, url: "mailto:niklasbuschmann@users.noreply.github.com"} - - {title: Github, icon: github, url: "https://github.com/niklasbuschmann/contrast"} - - {title: Subscribe, icon: rss, url: "/feed.xml"} + - {title: Mail, icon: envelope, url: "lehoangviet2k@gmail.com"} + - {title: Github, icon: github, url: "https://github.com/Mikyx-1"} + # - {title: Subscribe, icon: rss, url: "/feed.xml"} comments: # disqus_shortname: "" # see https://disqus.com/ diff --git a/_posts/2017-01-01-HOG.md b/_posts/2017-01-01-HOG.md new file mode 100644 index 00000000000..bf73cb6beed --- /dev/null +++ b/_posts/2017-01-01-HOG.md @@ -0,0 +1,22 @@ +--- +title: "Giải thích và code thuật toán HOG (Histogram of oriented gradient)" +mathjax: true +layout: post +categories: media +--- + +Xin chào các bạn, +Trong bài post này, mình sẽ giới thiệu một phương pháp "cổ điển" để extract features của một bức ảnh. Ở thời đại Deep Learning phát triển vượt bậc như hiện nay, các mạng CNN thường được dùng để trích xuất các đặc trưng của bức ảnh. Tuy nhiên, trước khi Yan LeCun đề xuất kiến trúc CNN thì cũng đã có nhiều nghiên cứu về phương pháp để trích xuất features của một bức ảnh và một trong những phương pháp phổ biến nhất đó là Histogram of Oriented Gradients (HOG). Chúng ta hãy cùng tìm hiểu xem HOG là gì và hoạt động như thế nào nhé. + +![Image](https://archive.ph/X93ra/a24f8024dd092236524506b3c9e19cd518045fc6.webp) + +### 1. Giới thiệu về HOG +HOG là một giải thuật trích xuất những điểm đặc trưng trong một bức ảnh và những đặc trưng này có thể được dùng để đưa vào các mô hình phân loại như SVM, Decision Tree, ... cho các task classification, detection, hay thậm chí segmentation. + +Điểm chính trong nguyên lý hoạt động c ủa HOG là mô tả hình dạng của một vật thể cục bộ thông qua hai ma trận: **magnitude gradient** và **orientation gradient*. Để tạo ra hai ma trận này, ảnh được chia thành một lưới ô vuông, trên mỗi ô vuông, ta tính toán biểu đồ histogram thống kê độ lớn gradient. Mỗi ô vuông thường có kích thước 8x8 pixels và bao gồm nhiều ô cục bộ. HOG descriptor được tạo thành bằng cách nối liền 4 vector histogram từ mỗi ô cục bộ. HOG descriptor được tạo thành bằng cách nối liền 4 vector histogram từ mỗi ô cục bộ thành một vector tổng hợp. Để cải thiện độ chính xác, mỗi giá trị của vector histogram trên mỗi vùng cục bộ được chuẩn hóa theo Norm 2 hoặc Norm 1. Ưu điểm của HOG là nó bao gồm tính bất biến đối với biến đổi hình học và thay đổi độ sáng, cũng như khả năng loại bỏ chuyển động cơ thể trong phát hiện con người nếu họ duy trì tư thế đứng thẳng. + +### 2. Cách hoạt động của HOG +Ở mục 1, mình đã giới thiệu tổng quan về HOG. Giờ thì hãy cùng đào sâu xem nó được hoạt động như thế nào nhé. + + + diff --git a/_posts/2017-01-01-advanced-examples.md b/_posts/2017-01-01-advanced-examples.md deleted file mode 100644 index 785d05464b8..00000000000 --- a/_posts/2017-01-01-advanced-examples.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: "Advanced examples" -mathjax: true -layout: post -categories: media ---- - -![Swiss Alps](https://user-images.githubusercontent.com/4943215/55412536-edbba180-5567-11e9-9c70-6d33bca3f8ed.jpg) - - -## MathJax - -You can enable MathJax by setting `mathjax: true` on a page or globally in the `_config.yml`. Some examples: - -[Euler's formula](https://en.wikipedia.org/wiki/Euler%27s_formula) relates the complex exponential function to the trigonometric functions. - -$$ e^{i\theta}=\cos(\theta)+i\sin(\theta) $$ - -The [Euler-Lagrange](https://en.wikipedia.org/wiki/Lagrangian_mechanics) differential equation is the fundamental equation of calculus of variations. - -$$ \frac{\mathrm{d}}{\mathrm{d}t} \left ( \frac{\partial L}{\partial \dot{q}} \right ) = \frac{\partial L}{\partial q} $$ - -The [Schrödinger equation](https://en.wikipedia.org/wiki/Schr%C3%B6dinger_equation) describes how the quantum state of a quantum system changes with time. - -$$ i\hbar\frac{\partial}{\partial t} \Psi(\mathbf{r},t) = \left [ \frac{-\hbar^2}{2\mu}\nabla^2 + V(\mathbf{r},t)\right ] \Psi(\mathbf{r},t) $$ - -## Code - -Embed code by putting `{{ "{% highlight language " }}%}` `{{ "{% endhighlight " }}%}` blocks around it. Adding the parameter `linenos` will show source lines besides the code. - -{% highlight c %} - -static void asyncEnabled(Dict* args, void* vAdmin, String* txid, struct Allocator* requestAlloc) -{ - struct Admin* admin = Identity_check((struct Admin*) vAdmin); - int64_t enabled = admin->asyncEnabled; - Dict d = Dict_CONST(String_CONST("asyncEnabled"), Int_OBJ(enabled), NULL); - Admin_sendMessage(&d, txid, admin); -} - -{% endhighlight %} - -## Gists - -With the `jekyll-gist` plugin, which is preinstalled on Github Pages, you can embed gists simply by using the `gist` command: - - - -## Images - -Upload an image to the *assets* folder and embed it with `![title](/assets/name.jpg))`. Keep in mind that the path needs to be adjusted if Jekyll is run inside a subfolder. - -A wrapper `div` with the class `large` can be used to increase the width of an image or iframe. - -![Flower](https://user-images.githubusercontent.com/4943215/55412447-bcdb6c80-5567-11e9-8d12-b1e35fd5e50c.jpg) - -[Flower](https://unsplash.com/photos/iGrsa9rL11o) by Tj Holowaychuk - -## Embedded content - -You can also embed a lot of stuff, for example from YouTube, using the `embed.html` include. - -{% include embed.html url="https://www.youtube.com/embed/_C0A5zX-iqM" %} diff --git a/_posts/2017-01-01-taylor_series.md b/_posts/2017-01-01-taylor_series.md new file mode 100644 index 00000000000..7f8c38b3cbc --- /dev/null +++ b/_posts/2017-01-01-taylor_series.md @@ -0,0 +1,16 @@ +--- +title: "Giải thích và code from scratch chuỗi Taylor (Taylor Series)" +mathjax: true +layout: post +categories: media +--- + + + +### References + +1. [Derivation of the Taylor Series Part 1](https://www.youtube.com/watch?v=2-X7lqZvjy8) + +2. [Derivation of Taylor Series Expansion](https://hep.physics.illinois.edu/home/serrede/P435/Lecture_Notes/Derivation_of_Taylor_Series_Expansion.pdf) + +3. [Taylor Series](https://byjus.com/maths/taylor-series/) \ No newline at end of file diff --git a/_posts/2017-02-01-TF-IDF.md b/_posts/2017-02-01-TF-IDF.md new file mode 100644 index 00000000000..2ab4084e5d0 --- /dev/null +++ b/_posts/2017-02-01-TF-IDF.md @@ -0,0 +1,17 @@ +--- +title: "Giải thích và code Term Frequency - Inverse Document Frequency" +layout: post +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ giới thiệu một phương pháp để xác định độ quan trọng của từng từ trong một câu. + +![Image](https://www.kdnuggets.com/wp-content/uploads/arya-tf-idf-defined-0-1024x573.png) + +Vậy mức độ quan trọng của một từ là gì? Mức độ quan trọng ở đây là một con số cụ thể nào đó và nếu nó lớn thì tức là tư đó quan trọng và ngược lại. Giả sử, bài phát biểu nhậm chức của tổng thống Mỹ như sau: "_Tôi sẽ tập trung vào y tế_", thì trong câu sau từ "_y tế_" nên có mức độ quan trọng lớn hơn các từ như "_Tôi_", "_sẽ_", "_tập_", "_trung_", "_vào_". + +### 1. Giới thiệu TF-IDF +Như tiêu đề thì TF-IDF là từ viết tắt của cụm Term Frequency - Inverse Document Frequency. Giả thuật này được hai nhà khpa học máy tính Hans Peter Luhn và Karen Spärck Jones tìm ra. Cụ thể hơn, Hans là người phát triển phần term frequency và Karen là người thêm phần Inverse Document Frequency vào giải thuật. + +![Image](https://spectrum.ieee.org/media-library/photo-ibm.jpg?id=25584953&width=1200&height=900) diff --git a/_posts/2017-02-01-markdown-examples.md b/_posts/2017-02-01-markdown-examples.md deleted file mode 100644 index 072a700e0ea..00000000000 --- a/_posts/2017-02-01-markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: "Markdown examples" -layout: post ---- - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - -Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. - - -## Heading Two (h2) - -### Heading Three (h3) - -#### Heading Four (h4) - -##### Heading Five (h5) - -###### Heading Six (h6) - - -## Blockquotes - -### Single line - -> My mom always said life was like a box of chocolates. You never know what you're gonna get. - -### Multiline - -> What do you get when you cross an insomniac, an unwilling agnostic and a dyslexic? -> -> You get someone who stays up all night torturing himself mentally over the question of whether or not there's a dog. -> -> – _Hal Incandenza_ - -## Horizontal Rule - ---- - -## Table - -| Title 1 | Title 2 | Title 3 | Title 4 | -|------------------|------------------|-----------------|-----------------| -| First entry | Second entry | Third entry | Fourth entry | -| Fifth entry | Sixth entry | Seventh entry | Eight entry | -| Ninth entry | Tenth entry | Eleventh entry | Twelfth entry | -| Thirteenth entry | Fourteenth entry | Fifteenth entry | Sixteenth entry | - -## Code - -Source code can be included by fencing the code with three backticks. Syntax highlighting works automatically when specifying the language after the backticks. - -```` -```javascript -function foo () { - return "bar"; -} -``` -```` - -This would be rendered as: - -```javascript -function foo () { - return "bar"; -} -``` - -## Lists - -### Unordered - -* First item -* Second item -* Third item - * First nested item - * Second nested item - -### Ordered - -1. First item -2. Second item -3. Third item - 1. First nested item - 2. Second nested item diff --git a/_posts/2017-03-01-welcome-to-jekyll.md b/_posts/2017-03-01-welcome-to-jekyll.md deleted file mode 100644 index 0b02eb48b78..00000000000 --- a/_posts/2017-03-01-welcome-to-jekyll.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: "Welcome to Jekyll" -layout: post ---- - -You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. - - -To add new posts, simply add a file in the `_posts` directory that follows the convention `YYYY-MM-DD-name-of-post.ext` and includes the necessary front matter. Take a look at the source for this post to get an idea about how it works. - -Jekyll also offers powerful support for code snippets: - -{% highlight ruby %} -def print_hi(name) - puts "Hi, #{name}" -end -print_hi('Tom') -#=> prints 'Hi, Tom' to STDOUT. -{% endhighlight %} - -Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. - -[jekyll-docs]: http://jekyllrb.com/docs/home -[jekyll-gh]: https://github.com/jekyll/jekyll -[jekyll-talk]: https://talk.jekyllrb.com/ diff --git a/_posts/2024-02-23-crawl_youtube_videos.md b/_posts/2024-02-23-crawl_youtube_videos.md new file mode 100644 index 00000000000..aa475c4bcc6 --- /dev/null +++ b/_posts/2024-02-23-crawl_youtube_videos.md @@ -0,0 +1,64 @@ +--- +title: "Hướng dẫn crawl Youtube Videos tự động với Python" +layout: post +--- + + +Xin chào các bạn! + +Trong bài post lần này mình sẽ giới thiệu một phương pháp để có thể tự động tải các videos/shorts/livestream từ Youtube về để có thêm nguồn data cho mô hình AI/ML. + +Trong lĩnh vực AI/ML, data chiếm 80% công việc của các kĩ sư nên việc biết tận dụng các nền tảng trực tuyến để có thể kiếm thêm nhiều data sẽ rất có ích cho việc xây dựng một mô hình tốt. Có thể kể đến một vài nền tảng nổi tiếng để chúng ta scrape dữ liệu như Instagram, Tiktok, Reddit, Twitter (X), ... + +Chúng ta bắt tay vào thực hành luôn nhé! + +**Bước 1**: Install thư viện scrapetube, youtube-dl với pip + +Cho bạn nào chưa biết, pip là một package manager của Python, nó giúp cho việc cài cắm đơn giản hơn rất nhiều so với việc ta tự download và install thủ công. + +Package đầu tiên chúng ta sẽ cài là scrapetube, thư viện này sẽ giúp chúng ta **trích xuất urls** của các videos/shorts một cách tự động mà không cần phải mở một trình duyệt phụ như thư viện Selenium. Vì vậy, việc dùng thư viện này sẽ tiết kiệm khá nhiều thời gian và băng thông mạng của các bạn 😄. Package thứ hai và cũng là package cuối cùng là youtube_dl, thư viện này sẽ giúp chúng ta **tải về các Youtube urls** đã được trích xuất dùng. + + +Ta dùng đoạn lệnh sau để có thể cài 2 thư viện này nhé + +``` +pip install scrapetube youtube_dl +``` +**Bước 2**: Code Python thôii + +```python +# Step 1: Import những thư viện cần thiết +import scrapetube +import requests +import subprocess +import argparse +import pandas as pd + +def download_video(url): + # Đoạn này để setup chất lượng video bạn muốn tải, bạn có thể tham khảo kĩ hơn tại đường link mình để bên dưới nha. + subprocess.call(["yt-dlp", "-f", "bestvideo[height<=1080][ext=mp4]", url]) + +channel_username = "schannelvn" # Ví dụ về kênh youtube, các bạn có thể lấy kênh nào tùy thích nhé + +# Có 3 options: videos, shorts, streams. Tùy các bạn muốn crawl dạng nào và bỏ vào content type nha, mặc định sẽ là videos. +urls = scrapetube.get_channel(channel_username=channel_username, content_type="videos") + + +# Bắt đầu download thôiii +for url in urls: + download_video(url) +``` + +**Kết luận** +Và thế là chỉ sau 2 bước đơn giản, các bạn có thể crawl hàng tấn videos để có thêm data cho model rồi. Đơn giản phải không nào 😄 + +Phần tiếp theo, mình sẽ hướng dẫn các bạn crawl tiktok videos full HD, không watermarks nha, nhớ ghé blog tiếp hen !! + + +### Tải liệu tham khảo +1. [Scrapetube-demasmid][Scrapetube-demasmid] +2. [Youtube-dl][youtube-dl] + + +[Scrapetube-demasmid]: https://github.com/dermasmid/scrapetube +[youtube-dl]: https://github.com/ytdl-org/youtube-dl diff --git a/_posts/2024-03-21-GuidedFilter.md b/_posts/2024-03-21-GuidedFilter.md new file mode 100644 index 00000000000..1c75e485f56 --- /dev/null +++ b/_posts/2024-03-21-GuidedFilter.md @@ -0,0 +1,184 @@ +--- +title: "Giải thích và code paper Guided Image Filtering" +layout: post +mathjax: true +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ giới thiệu một thuật toán image filtering mới tên là Guided Image Filtering. Đây là thuật toán làm mịn ảnh tốt nhất tính đến thời điểm bài post này được viết. Phương pháp này được đề xuất bởi Kaiming He (cũng là cha đẻ của mô hình ResNet), Jian Sun, và Xiaoou Tang tại hội nghị ECCV 2010. Cũng đã thấm thoát 14 năm trôi qua, đã có nhiều biến thể của thuật toán này như Fast Guided Image Filtering, Deep Guided Image filtering, ... được đề xuất, tuy nhiên chúng đều dựa trên nền của giải thuật gốc. Đây là một giải thuật đạt được cùng một lúc cả hai tiêu chí: **nhanh hơn và tốt hơn**. + +### 1. Tại sao Guided Image Filtering được ra đời ? + +Thuật toán Guided Image Filtering ra đời với mục tiêu giải quyết một số thách thức quan trọng trong xử lý ảnh, đặc biệt là trong việc làm mịn ảnh. Trước đó, các phương pháp truyền thống như bộ lọc trung bình, Gaussian Blur, bilateral filter lọc nhiễu và làm mịn ảnh nhưng đồng thời cũng làm mất các chi tiết trong ảnh. Để khắc phục nhược điểm đó của các bộ lọc trên, bộ lọc Guided filter tích hợp thêm thông tin của ảnh gốc để có thể "hướng dẫn" giải thuật lọc đi đúng hướng và giữ được nhiều chi tiết nhất có thể. + +Ngoài ứng dụng làm mịn, Guided filter còn có thể được ứng dụng cho colorisation, image matting, multi-scale decomposition, và haze removal. + +
+ +
Ứng dụng của guided filter trong image matting. Nguồn: claude_ssim.log +
+
+ + +### 2. Giải thuật Guided Image Filtering + +
+ +
Minh họa behaviour của guided filter Nguồn: ResearchGate +
+
+ +Guided filter hoạt động giống với tích chập ở chỗ nó cũng lướt qua từng patch của tấm ảnh. + +Giả sử, $$ q $$ là output, $$I$$ là guidance image, $$I$$ là ảnh đầu vào. $$w_k$$ là một cửa sổ (kernel). + +Về tổng quát, Guided filter có công thức như sau: + +$$q_i = a_kI_i + b_k$$ + +Với $$(a_k, b_k)$$ là trọng số tuyến tính (kiểu $$y = ax + b$$) trong kernel $$w_k$$. Tham số $$r$$ là bán kính của kernel. Kiểu công thức tuyến tính này sẽ giúp cho ảnh output giữ lại được các cạnh và chi tiết có tần số cao trong ảnh được filtered bởi vì $$\nabla q = a\nabla I$$. Công việc của chúng ta là tìm ra hệ số $$a_k \text{ và } b_k$$ cho mỗi kernel. Việc tìm ra hai hệ số này dựa vào hàm loss sau: + +$$E(a_k, b_k) = \sum_{i \in w_k}{((a_kI_i + b_k - p_i)^2 + \varepsilon a_k^2)}$$ + +Hàm loss này có mục đích tổi thiểu hóa khoảng cách L2 của output với ảnh guidance. Hệ số $$\varepsilon$$ ở đây được tác giả thêm vào để tránh cho $$a_k$$ trở nên quá lớn. + +Chúng ta có thể dùng nhiều phương pháp để có thể tìm ra 2 hệ số này bằng nhiều phương pháp như Gradient Descent, Newton, ... Tuy nhiên, tác giả cũng đã cung cấp công thức closed form để tìm ra $$a_k, b_k$$ trong paper của mình. Cụ thể, với mỗi kernel 2 thông số được tính như sau: + +$$a_k = \frac{\frac{1}{|w|}\sum_{i \in w_k}{I_ip_i - \mu _k \overline{p_k}}}{\sigma _k ^2 + \epsilon}$$ + +$$b_k = \overline{p_k} - a_k \mu _k$$ + +Với $$\mu_k, \sigma_k$$ là giá trị mean và variance của ảnh input trong kernel $$w_k$$, $$\vert w \vert$$ là tổng số lượng pixels trong kernel, $$\overline{p_k}$$ là giá trị trung bình của các pixels của ảnh guidance trong kernel $$w_k$$. + +Tuy nhiên, có một vấn đề là một pixel bị overlapped bởi nhiều kernel giống như tích chập vậy. Để giải quyết vấn đề này, chúng ta sẽ lấy giá trị trung bình của chúng và quan hệ của input và output sẽ chỉ còn $$\nabla q \approx a \nabla I$$. Tuy nhiên, như vậy cũng đã đủ tốt để giữ lại các chi tiết có tần số cao trong tấm ảnh. + +Cuối cùng, ta có công thức tổng quát cho guided filter như sau: + +$$q_i = \frac{1}{|w|}\sum_{i \in w_k}{a_kI_i + b_k} = \overline{a_i}I_i + \overline{b_i}$$ + +* Chứng minh kết quả tìm $$a_k$$ và $$b_k$$ của tác giả (Optional) + +$$E(a_k, b_k) = \sum_{i \in w_k}{((a_kI_i + b_k - p_i)^2 + \varepsilon a_k^2)}$$ + +$$E(a_k, b_k) = \sum_{i \in w_k}{(a_k^2 I_i^2 + b_k^2 + p_i^2 + 2a_k b_k I_i - 2 b_k p_i - 2 a_k I_i p_i + \varepsilon a_k^2)}$$ + +$$E(a_k, b_k) = \sum_{i \in w_k}{(a_k^2 I_i^2 + b_k^2 + p_i^2 + 2a_k b_k I_i - 2 b_k p_i - 2 a_k I_i p_i + \varepsilon a_k^2)}$$ + +Đặt $$\alpha = \sum_{i \in w_k}I_i^2$$, $$\beta = \sum_{i \in w_k} I_i p_i$$, $$\gamma = \sum_{i \in w_k} p_i$$, ta có biến đổi như sau: + +$$E(a_k, b_k) = a_k^2 \alpha + \vert \omega \vert b_k^2 + \sum_{i \in w_k} p_i^2 + 2 a_k b_k \sum_{i \in w_k}I_i - 2 a_k \beta - 2b_k \gamma + \varepsilon a_k^2$$ + +Lấy đạo hàm từng phần để biết cực tiểu của hàm theo $$a_k$$ và $$b_k$$: + +$$ +\begin{aligned} +\frac{\partial E}{\partial a_k} +&= 2a_k \alpha + 2 b_k \sum_{i \in w_k} I_i - 2 \beta + 2 \varepsilon a_k = 0 \\ +&= a_k(\alpha + \varepsilon) + b_k \sum_{i \in w_k} I_i - \beta = 0 \\ +&= a_k(\alpha + \varepsilon) + b_k \sum_{i \in w_k} I_i = \beta +\end{aligned}$$ + +$$ +\begin{aligned} +\frac{\partial E}{\partial b_k} +&= 2 b_k \vert \omega \vert + 2a_k \sum_{i \in w_k} I_i - 2 \gamma = 0\\ +&= b_k \vert \omega \vert + a_k \sum_{i \in w_k} I_i = \gamma +\end{aligned}$$ + +Từ 2 phương trình trên, ta sẽ kết hợp giải phương trình để tìm ra $$a_k^*$$ và $$b_k^*$$. + +$$b_k = \frac{\gamma - a_k \sum_{i \in w_k}I_i}{\vert \omega \vert}$$ + +Thay vào, ta được + +$$a_k (\alpha + \varepsilon) - (\frac{\gamma - a_k \sum_{i \in w_k}I_i}{\vert \omega \vert}) \sum_{i \in w_k} I_i = \beta$$ + +$$a_k (\alpha + \varepsilon - \frac{\sum_{i \in w_k} I_i\sum_{i \in w_k} I_i}{\vert \omega \vert}) = \beta + \frac{\gamma \sum_{i \in w_k} I_i}{\vert \omega \vert}$$ + +Ta chia 2 vế cho $$\vert \omega \vert$$ và bắt đầu biến đổi. Đầu tiên, ta sẽ biến đổi vế phải trước. + +$$\frac{\beta}{\vert \omega \vert} + \frac{\gamma \sum_{i \in w_k} I_i}{\vert \omega \vert ^2} = \frac{\sum_{i \in w_k} I_i p_i}{\vert \omega \vert} + \frac{\sum_{i \in w_k} p_i \sum_{i \in w_k} I_i}{\vert \omega \vert ^ 2}$$ + +Với vế trái, sẽ dễ dàng hơn nếu ta biến đổi ngược: + +$$ +\begin{aligned} +\sigma_I^2 +&= \frac{\sum_{i \in w_k} (I_i - \overline{I})^2}{\vert \omega \vert} \\ +&= \frac{\sum_{i \in w_k} (I_i^2 + \overline{I}^2 - 2I_i\overline{I})}{\vert \omega \vert} \\ +&= \frac{\sum_{i \in w_k} I_i^2}{\vert \omega \vert} + \frac{\sum_{i \in w_k} (\overline{I}^2 - 2I_i \overline{I})}{\vert \omega \vert} \\ +&= \frac{\sum_{i \in w_k} I_i^2}{\vert \omega \vert} + \frac{\vert \omega \vert \overline{I}^2 - 2 \overline{I} \sum_{i \in w_k} I_i}{\vert \omega \vert} \\ +&= \frac{\sum_{i \in w_k} I_i^2}{\vert \omega \vert} + \overline{I}^2 - 2\overline{I}^2 \\ +&= \frac{\sum_{i \in w_k} I_i^2}{\vert \omega \vert} - \overline{I}^2 \\ +&= \frac{\sum_{i \in w_k} I_i^2}{\vert \omega \vert} - \frac{\sum_{i \in w_k} I_i \sum_{i \in w_k} I_i}{\vert \omega \vert ^ 2} \\ +&= \frac{\alpha}{\vert \omega \vert} - \frac{\sum_{i \in w_k} I_i \sum_{i \in w_k} I_i}{\vert \omega \vert ^ 2} \quad = \text{vế trái (đpcm)} +\end{aligned}$$ + +$$ +\begin{aligned} +\rightarrow a_k +&= \frac{\frac{\sum_{i \in w_k} I_i p_i}{\vert \omega \vert} + \frac{\sum_{i \in w_k} p_i \sum_{i \in w_k} I_i}{\vert \omega \vert ^ 2}}{\sigma_I^2 + \varepsilon} \\ + +&= \frac{\frac{1}{|w|}\sum_{i \in w_k}{I_ip_i - \mu _k \overline{p_k}}}{\sigma _I ^2 + \epsilon} \\ +\end{aligned}$$ + +$$ +\begin{aligned} +b_k +&= \frac{\gamma - a_k \sum_{i \in w_k} I_i}{\vert \omega \vert} \\ +&= \frac{\sum_{i \in w_k} p_i - a_k \sum_{i \in w_k} I_i}{\vert \omega \vert} \\ +&= \overline{p_k} - a_k \mu _k +\end{aligned}$$ + +### 3. Implementation + +```python + +def guided_filter(p, I, r, e): + ''' + Guided filter implemented by Mikyx-1 + 21/04/2024 + Args: + + p (torch.Tensor): Guidance Image + I (torch.Tensor): Input Image + r (int): Kernel Size (!= radius in the paper) + e (float): epsilon to prevent underflow + + Returns: + Output (torch.Tensor): Output image in format BCHW + ''' + kernel = torch.ones((I.shape[0], 1, r, r))/(r**2) + padding = r//2 + + assert p.shape == I.shape, "Shapes do not match, retry!" + + meanI = F.conv2d(I, kernel, padding=padding, stride=1) + meanP = F.conv2d(p, kernel, padding=padding, stride=1) + corrI = F.conv2d(I*I, kernel, padding=padding, stride = 1) + corrIp = F.conv2d(I*p, kernel, padding=padding, stride = 1) + + sigmaK = corrI - meanI*meanI + a = (corrIp - meanI*meanP)/(sigmaK + e) + b = meanP - a*meanI + + meanA = F.conv2d(a, kernel, padding=padding, stride = 1) + meanB = F.conv2d(b, kernel, padding=padding, stride = 1) + + output = meanA*I + meanB + return output +``` + +### 4. Kết luận + +Bộ lọc guided filter là một bộ lọc có độ phức tạp $$O(N)$$ nên có tốc độ gần như bằng các bộ lọc Gaussian, mean. Ngoài ra, bộ lọc guided filter là một bộ lọc đa năng và có thể dùng trong rất nhiều trường hợp khác nhau. Ví dụ, bộ lọc guided filter cũng sẽ rất hữu dụng trong việc làm mịn các tín hiệu 1D (có thể cạnh tranh với các bộ lọc 1 Euro, Kalman filter, ...). + +Trong bài viết này, mình đã trình bày cách hoạt động, chứng minh bộ lọc guided filter và cũng như là code from scratch. Hy vong các bạn cảm thấy hữu ích. Peace. + +### References + +1\. [Guided Image filtering - Kaiming He, Jian Sun, Xiaoou Tang][paper] + +[paper]: https://people.csail.mit.edu/kaiming/publications/eccv10guidedfilter.pdf + diff --git a/_posts/2024-03-21-NMS_explained.md b/_posts/2024-03-21-NMS_explained.md new file mode 100644 index 00000000000..8fa455682de --- /dev/null +++ b/_posts/2024-03-21-NMS_explained.md @@ -0,0 +1,242 @@ +--- +title: "Giải thích và code Non-Maximum Suppression from scratch" +layout: post +--- + + +Xin chào các bạn! + +Trong bài post này, mình sẽ giải ngố về giải thuật non-maximum suppression (NMS) thường được dùng trong khâu post-processing của YOLO models. + + +### 1. NMS giải quyết vấn đề gì ? +Nếu các bạn đã từng dùng các thư viện Ultralytics để chạy các mô hình YoloV8, Yolov5 hoặc các repo Yolo trên github, thì output YOLO xuất ra cho chúng ta thường nằm ở định dạng x_center, y_center, width, height, class, confidence_score. Tuy nhiên, đó là những output đã được hậu xử lý, và khi các bạn chuyển qua các định dạng khác như ONNX, tflite, TensorRT, OpenVINO, ... để deploy trên các ngôn ngữ khác như Java, Javascript, C++ thì các output của model sẽ nằm ở dạng 8400 x Num_class + 4. Lý do của việc này là vì sau khi convert model qua ngôn ngữ khác thì khâu hậu xử lý mà tác giả viết (thường bằng Python) sẽ không được đính kèm theo và điều này buộc chúng ta phải viết lại khâu này bằng các ngôn ngữ chúng ta deploy. Giả sử chúng ta muốn deploy model detection lên web thì phải viết lại khâu hậu xử lý bằng ngôn ngữ Javascript, hoặc trên thiết bị nhúng bằng C++. Và điều này đòi hỏi bạn phải hiểu được thuật toán NMS để có thể viết lại. + + +### 2. Intersection over Union (IoU) +Để có thể dễ dàng hiểu và implement được NMS thì chúng ta cần phải hiểu được cách xấc định IoU của 2 bounding boxes. May mắn là IoU cực kì đơn giản. + +![Hình1](https://b2633864.smushcdn.com/2633864/wp-content/uploads/2016/09/iou_equation.png?lossy=2&strip=1&webp=1) + + +IoU sẽ được tính bằng cách lấy diện tích của phần giao chia cho diện tích của phần hợp. + +![Hình2](https://viso.ai/wp-content/uploads/2024/01/Illustrative-Example-for-IoU-Calculation.jpg) + + +Như ở hình trên, giao của 2 bounding box là một bounding box có góc trái (x3, y3) và góc phải là (x2, y2) và diện tích của nó là H x W = (200-80)x(300-120) = 21600. Diện tích phần hợp bằng diện tích của 2 hình trừ đi diện tích của hình giao, vì vậy sẽ được tính như sau (200-50) x (300 - 100) + (220 - 80) x (310 - 120) - 21600 = 30000 + 26600 - 21600 = 35000. Lấy diện tích phần giao chia cho diện tích phần hợp, ta sẽ được ~0.6171 là IoU của 2 bounding boxes trên. + +### 3. Thuật toán NMS hoạt động như thế nào ? +Như mình đã đề cập ở mục 1, output của mô hình YOLO có định dạng 8400 x Num_class + 4 và NMS sẽ giúp chúng ta chuyển từ định dạng này về định dạng chúng ta thường thấy là x_center, y_center, width, height. + +![Hình1](https://cdn.analyticsvidhya.com/wp-content/uploads/2020/07/Screenshot-from-2020-07-27-20-37-58.png) NMS lọc ra những bounding boxes tốt nhất từ một rừng những detections + + +8400 là con số lượng detection boxes ở hình bên trái với chi chít ô vuông, Num_class + 4 là một vector chứa các giá trị classification score của mỗi class và 4 giá trị x_center, y_center, x_center, y_center. Vì vậy, nếu bạn huấn luyện để detect 2 class thì vector này sẽ có 6 giá trị. Ví dụ, nếu vector có dạng [0.009, 0.073, 0.2, 0.2, 0.3, 0.3] thì tức là classification score của class 1 là 0.009, class 2 là 0.073 và x_center nằm ở vị trí 0.2 độ rộng của bức ảnh, y_center nằm ở vị trí 0.2 độ dài của bức ảnh, và width và height của bounding box lần lượt là 0.3, 0.3. Vì vậy, thuật toán NMS sẽ giúp ta tìm ra những bounding boxes tốt nhất cho chúng ta như các bạn thấy ở hình bên phải. + +**Note**: Nếu các bạn thắc mắc tại sao classification scores của các class không có tổng bằng 1 thì lý do là mô hình YOLO dùng Sigmoid activation cho mỗi class, vì vậy tổng của chúng sẽ có thể không bằng 1 như Softmax. + +Khâu hậu xử lý trong YOLO gồm 5 bước, + +**Step 1**: Lọc những detections có confidence score thấp +Trong settings của YOlO có một parameter là **conf_thresh**, tức là max confidence score để nó được chuyển qua step 2 để xử lý. Giả sử, nếu bạn set conf_thresh = 0.5 thì các detection với max confidence giữa các class là 0.45 thì detection này sẽ bị loại và ngược lại. Để minh họa, giả sử chúng ta có 3 class: người, chó, mèo và detection vector ở dạng sau [0.16, 0.08, 0.32, 0.2, 0.2, 0.3, 0.3] thì tức detection vector này sẽ bị loại vì max confidence score chỉ có 0.32. + +**Step 2**: Ở bước này, ta sẽ dùng hàm argmax để tìm ra class có confidence score cao nhất của một detection. Ví dụ minh họa, nếu ta có detection [0.92, 0.8, 0.5, 0.2, 0.2, 0.3, 0.3] thì class đầu tiên sẽ được chọn. Và vector này sẽ được chuyển thành [0.2, 0.2, 0.3, 0.3, 1, 0.92], với 4 số đầu là vị trí, số gần cuối là class_id, và số cuối cùng là confidence score. + +**Note**: Mình để step 2 để các bạn dễ hình dung chứ chính xác thì nó được chạy cùng với step 1 luôn để tiết kiệm chi phí tính toán. + +**Step 3**: Ở bước 3, ta sẽ dùng một hàm để lọc ra các class như những bounding boxes thuộc class 1, những bounding boxes thuộc class 2, ... + +**Step 4**: Ở bước này, ở mỗi class ta sẽ loop qua các detection và tính IoU của 2 bounding boxes và nếu IoU lớn hơn một khoảng định trước (trong setting của YOLO là iou_thresh) thì 2 bounding boxes đó được xem là trùng và chúng ta chỉ lấy 1 bounding box mà có confidence score cao nhất trong số đó ra thôi. Nếu không trùng thì ta sẽ lấy cả 2. + +**Step 5**: Tập hợp lại những detections ở các class và đó là output mà các bạn thường thấy khi dùng Ultralytics. + + +### 4. Coding time + +Khi chuyển qua các format khác thì chúng ta sẽ bị mất khâu hậu xử lý, vì vậy mình sẽ hướng dẫn các bạn biến phần output thô này thành thông tin chúng ta mong muốn. Mình sẽ demo ví dụ dưới đây với format ONNX, format ONNX là format phổ biến nhất có thể deploy trên rất nhiều ngôn ngữ và platform. Và ở đây, mình sẽ perform class-aware NMS, class-agnostic NMS cũng tương tự tuy nhiên dễ hơn nha. + +Đầu tiên, chúng ta hãy import những thư viện cần thiết và model, + +```python +import numpy as np +import matplotlib.pyplot as plt +import cv2 +from ultralytics import YOLO +import onnxruntime as ort + +model = ort.InferenceSession("./yolov8n.onnx", providers = ["CPUExecutionProvider"]) + +inputs = model.get_inputs() +input = inputs[0] +print(f"Input Name: ", input.name) +print(f"Input Type: ", input.type) +print(f"Input Shape: ", input.shape) + +outputs = model.get_outputs() +output = outputs[0] +print(f"Output Name:",output.name) +print(f"Output Type:",output.type) +print(f"Output Shape:",output.shape) +``` + +``` +> Input Name: images +> Input Type: tensor(float) +> Input Shape: [1, 3, 640, 640] +> Output Name: output0 +> Output Type: tensor(float) +> Output Shape: [1, 84, 8400] +``` + +Chạy inference để xem output của model có gì nào, mình sẽ lấy tấm hình này để + demo +![Hình](https://res.cloudinary.com/practicaldev/image/fetch/s--Hj3v1AXE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7ndyaueoc8elyqtq69u.jpg) +```python +image = cv2.imread("./demo.jpg") +print(f"Raw image shape: {image.shape}") +rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) +transformed_image = (cv2.resize(rgb_image, (640, 640)).astype("float32")/255.).transpose(2, 0, 1)[None, ...] +print(f"Transformed image shape: {transformed_image.shape}") +``` + +``` +> Raw image shape: (415, 612, 3) +> Transformed image shape: (1, 3, 640, 640) +``` + +```python +pred = model.run(["output0"], {"images": transformed_image})[0] +print(f"Shape of prediction: {pred.shape}") +``` + +``` +> Shape of prediction: (1, 84, 8400) +``` + +```python +pred = pred.transpose(0, 2, 1)[0] +``` + +Đây là function sẽ thực hiện bước 1 và bước 2 + +```python +def parse_pred(pred, img_width, img_height): + + # Trích xuất max confidence score của mỗi box + max_confidence_score_each_box = np.max(pred[:, 4:], axis = 1) + + # Trích xuất ra indexes có max confidence score > 0.5, cái này các bạn có + # thể set tùy ý, mình recommend > 0.4 + chosen_box_inds = np.where(max_confidence_score_each_box > 0.5)[0] + + # Chọn ra những detections có max confidence score > 0.5 + chosen_detections = pred[chosen_box_inds] + + # Trích ra index của các class trong các detections được lọc + detection_classes = chosen_detections[:, 4:].argmax(axis = 1) + + # Trích ra những confidence score cao nhất của box đó + detection_highest_confidence_scores = chosen_detections[:, 4:].max(axis = 1) + + # Rescale về tọa độ của ảnh gốc + x1 = ((chosen_detections[:, 0] - chosen_detections[:, 2]/2)/640)*img_width + y1 = ((chosen_detections[:, 1] - chosen_detections[:, 3]/2)/640)*img_height + + x2 = ((chosen_detections[:, 0] + chosen_detections[:, 2]/2)/640)*img_width + y2 = ((chosen_detections[:, 1] + chosen_detections[:, 3]/2)/640)*img_height + + + return np.hstack([x1[..., None], y1[..., None], + x2[..., None], y2[..., None], + detection_classes[..., None], + detection_highest_confidence_scores[..., None]]) +``` + + +Đây là function sẽ thực hiện giải thuật non-maximum suppression cho 1 class + +```python +def nms_for_one_class(boxes, scores, iou_threshold): + # Trích xuất vị trí các tọa độ + x1 = boxes[:, 0] + y1 = boxes[:, 1] + x2 = boxes[:, 2] + y2 = boxes[:, 3] + + # Tính diện tích các area của các detections + areas = (x2 - x1 + 1)*(y2 - y1 + 1) + + # Sort những indexxes có confidence score cao + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + + xx1 = np.maximum(x1[i], x1[order[1:]]) + yy1 = np.maximum(y1[i], y1[order[1:]]) + + xx2 = np.minimum(x2[i], x2[order[1:]]) + yy2 = np.minimum(y2[i], y2[order[1:]]) + + width = np.maximum(0.0, xx2 - xx1 + 1) + height = np.maximum(0.0, yy2 - yy1 + 1) + + intersection = width*height + + iou = intersection / (areas[i] + areas[order[1:]] - intersection) + # Nếu IoU nhỏ hơn iou_threshold thì sẽ lưu vào + inds = np.where(iou <= iou_threshold)[0] + order = order[inds + 1] + + return np.array(keep, dtype=np.int32) +``` + + +Đây là giải thuật thực hiện khâu cuối cùng của hậu xử lý. Hàm này có tác dụng lọc ra những detection của từng class và chạy nms trên những detection của class đó. Và sau khi chạy trên tất cả các class, ta sẽ thu về được kết quả sau: + +```python +def last_step(parsed_pred): + unique_classes = np.unique(parsed_pred[:, -2]) + finest_idxes = np.array([]).astype(int) + for unique_class in unique_classes: + uc_idxes = np.where(parsed_pred[:, -2] == unique_class)[0] + uc_detections = parsed_pred[uc_idxes] + boxes = uc_detections[:, :4] + scores = uc_detections[:, -1] + after_nms = nms_for_one_class(boxes, scores, iou_threshold = 0.7) + finest_idxes = np.append(finest_idxes, values = after_nms) + return parsed_pred[finest_idxes] +``` + +Đây là functions hoàn chỉnh, +```python +parsed_pred = parse_pred(pred, image.shape[1], image.shape[0]) +final_pred = last_step(parsed_pred) +print(f"Prediction after post-processing: {final_pred}") +``` + +``` +> array([[ 140.52, 169.65, 255.55, 316.71, 17, 0.61475], + [ 262.22, 94.908, 460.41, 315.78, 16, 0.78507]]) +``` + +Trong COCO class, class 16 là dog và 17 là cat, vì vậy trong tấm ảnh model detect được 2 vật thể là chó và mèo. Giờ chúng ta visualise kết quả sau khi vẽ prediction nhé. + +![Hình](https://res.cloudinary.com/practicaldev/image/fetch/s--H-jJ95dA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/seng6q3rgczha3aov7j8.png) + +Và thế là, sau một vài steps, ta đã thực hiện xong khâu hậu xử lý của YOLO!! + +P/s: Nếu các bạn thắc mắc tại sao hai tấm ảnh cùng kích thước nhưng một tấm lại xử lý lâu hơn và tấm kia xử lý nhanh hơn thì đó chủ yếu là do khối hậu xử lý của YOLO nha. Vì có tấm ở step 1 và 2 đã lọc ra còn khoảng 3-4 detections nhưng có tấm sau step 1 và 2 vãn còn 400-500 detections. + + +### Tài liệu tham khảo + +1. [Ultralytics YOLO][Ultralytics YOLO] +2. [How to create YOLOv8-based object detection web service using Python, Julia, Node.js, JavaScript, Go and Rust][How to create YOLOv8-based object detection web service using Python, Julia, Node.js, JavaScript, Go and Rust] + + +[Ultralytics YOLO]: https://github.com/ultralytics/ultralytics +[How to create YOLOv8-based object detection web service using Python, Julia, Node.js, JavaScript, Go and Rust]: https://dev.to/andreygermanov/how-to-create-yolov8-based-object-detection-web-service-using-python-julia-nodejs-javascript-go-and-rust-4o8e#process_the_output_nodejs diff --git a/_posts/2024-03-21-transformer.md b/_posts/2024-03-21-transformer.md new file mode 100644 index 00000000000..d52f0a15c5a --- /dev/null +++ b/_posts/2024-03-21-transformer.md @@ -0,0 +1,586 @@ +--- +title: "Giải thích và code Transformers" +mathjax: true +layout: post +--- + +Xin chào các bạn! + +Trong bài post này, mình sẽ giới thiệu về kiến trúc transformers. Đây được xem là một trong những model có tính cách mạng nhất trong deep learning vì nó đã tạo tiền đề cho sự ra đời của các mô hình ngôn ngữ lớn hay các mô hình SOTA cho các task như speech recognition, computer vision, ... + +### 1. Một chút về lịch sử của transformers + +Mô hình này được ra mắt nhằm thay thế cho kiến trúc Recurrent neural networks (RNNs), vốn đã được ra mắt vào những năm 90. Sơ lược thì kiến trúc recurrent này được thiết kế ra nhằm xử lý các loại thông tin có thứ tự như text, speech. Tuy nhiên, kiến trúc này có nhược điểm là vấn đề về **exploding/vanishing gradient** khi training nên nó không thể xử lý một đoạn thông tin quá dài. Vì những lý do đó mà Large language models không tồn tại trước khi transformer được ra mắt. + +Đến năm 2014, một nhóm nghiên cứu ở Google đã nghiên cứu những phương pháp để khắc phục vấn đề trên của RNNs và đề xuất kiến trúc transformers. Chỉ sau 10 năm ra mắt, những thành tựu đạt được từ kiến trúc transformers nhiều không xuể với sự ra mắt của các mô hình ngôn ngữ lớn như ChatGPT, Llama3, Segment Anything, ... + + +### 2. Ý tưởng chính của transformers + +Ý tưởng chính của Transformers là cách sử dụng thông minh và khéo léo các phép tính toán song song (như nhân ma trận, ánh xạ, ...) để thay thế các tính toán kiểu one-by-one của RNNs nhằm giải quyết vấn đề **exploding/vanishing gradient**. + + +### 2.1. Positional embedding + +Transformers xử lý text một cách song song thay vì tuần tự như kiểu RNN, tức là nó sẽ cho nguyên cả đoạn text vào input cho 1 lần xử lý thay vì chia ra thành các timestep. Và vì trong các tín hiệu tuần tự thì thứ tự là một thông tin rất quan trọng. Ví dụ như "xin chào bạn" và nếu ta cho nó một thứ tự khác thì sẽ được câu như "chào xin bạn", "bạn xin chào". Như vậy, **thứ tự có tính chất rất quan trọng để giữ lại được ngữ nghĩa của input**. Khi xử lý tuần tự như kiểu RNNs thì ta không cần thông tin thứ tự vì timestep vốn đã là thứ tự rồi. Tuy nhiên đối với mô hình xử lý cả câu, cả đoạn một lúc như transformers thì phải có một thông tin thứ tự được chèn thêm vào để giúp mô hình này có thể biết được vị trí chính xác của từng từ trong input. Ashish Vaswani (1st author của paper) đã dùng một phương pháp để thực hiện việc này có tên là **positional embedding**. + + +* **Công thức toán học của Positional Embedding** + +$$PE_{(pos, 2i)} = sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}})$$ + + +$$PE_{(pos, 2i + 1)} = cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})$$ + +Với, + +$$pos: \text{Vị trí của phần từ trong input. Ex: 1, 2, ..., sequence length}$$ + +$$i: \text{Dimension thứ i của phần từ trong input: Ex: 1, 2, ...,} d_{model}$$ + +$$d_{model}: \text{Số dimension của input. Ex: 128 or 256 or 512 or any number}$$ + +### 2.2. Self-Attention + +Trong các dữ liệu tuần tự như kiểu văn bản thì ngữ nghĩa của một từ ngoài phụ thuộc vào chính nó mà còn phụ thuộc vào các element đứng trước và sau. Ví dụ trong 2 câu sau "book a room" và "book in the room" thì từ book có 2 nghĩa hoàn toàn khác nhau. Để có thể phân biệt được từ "book" trong câu có nghĩa là _đặt_ hay là _quyển sách_, ta phải dựa vào các từ kế bên trái và phải để có thể biết được. Và vì cần một cơ chế để có thể giúp nhận ra nghĩa của từ hiện tại dựa vào các từ bên cạnh, tác giá đã đề xuất cơ chế **self-attention**. + +Một cách ngắn gọn, cơ chế self-attention giúp xác định ngữ nghĩa của 1 từ trong câu rõ ràng hơn. Self-attention cho biết sự "liên quan" của từ hiện tại với các từ còn lại trong input. Ví dụ self-attention của một input là text sẽ có dạng như sau: + +
+Kiến trúc của LeNet-5 và AlexNet +
Hình 2.2.1. Ví dụ minh họa về self-attention - Google
+
+ +Ở câu trong hình trên, chỉ khác nhau ở 2 từ ở cuối câu là "tired" và "wide". Nếu từ cuối cùng là "tired" thì từ "it" có attention score cao nhất (đường càng đậm thì có giá trị càng lớn) ở từ animal (ám chỉ rằng nó đang nói về animal) trong khi đó nếu từ cuối cùng là "wide" thì từ "it" sẽ có attention score cao nhất ở từ "street" nhằm ám chỉ rằng từ này đang có liên quan nhiều với từ "street" trong câu. + +Nhờ có kiến trúc self-attention này, các ngữ nghĩa của 1 từ và các thông tin xung quanh được tích hợp lại một cách chặt chẽ đóng vai trò chính trong sự vượt trội của mô hình transformers so với các mô hình tiền nhiệm nó. + + +* **Công thức toán học của Self-Attention** + +$$Attention(Q, K, V) = softmax(\frac{QK^{T}}{\sqrt d_k})V$$ + +Trong đó, + +$$Q: Query, \R ^ {N \times d_k}$$ + +$$K: Key, \R ^ {N \times d_k}$$ + +$$V: Value, \R ^ {N \times d_v }$$ + +$$d_k: \text{Key dimension}$$ + + +### 2.3. Multi Head Attention + +Trong paper của mình, tác giả Ashish Vaswani cho rằng việc dùng thêm Linear layer để ánh xạ nó lên một không gian có h x d_embedding rồi thực hiện self-attention trên h x d_embedding đó sẽ "_jointly attend to information from different representation +subspaces at different positions_". Mình không biết dịch đúng nghĩa đoạn trên như thế nào, nhưng ý tác giả muốn truyền tải rằng việc này sẽ giúp thông tin trong input kết nối với nhau ở nhiều chiều không gian hơn. + +Với Multi Head Attention, tác giả dùng nhiều khối self-attention rồi ghép nối kết quả lại với nhau. Cụ thể, tác giả sẽ dùng _h_ khối self-attention cho Q, K, V và sau đó concat kết quả cuối cùng. + +
+ +
Hình 2.3.1. Khối self-attention (trái) và multi head attention (phải)
+
+ +* **Công thức toán học của Multi Head Attention** + +$$MultiHead(Q, K, V) = Concat(head_1, ..., head_h)W^O$$ + +Với, + +$$head_i = Self Attention(QW_i^Q, KW_i^K, VW_i^V)$$ + +Trong đó, + +$$W_i^Q \in \R^{d_{model} \times d_k}, W_i^K \in \R ^{d_{model} \times d_k}, W_i^V \in \R ^{d_{model} \times d_v}, W^O \in \R^{hd_v \times d_{model}}$$ + + + +### 3. Xây dựng và train transformer với Pytorch + +Trong phần này, mình sẽ xây dựng kiến trúc transformer from scratch và train nó với 2 tasks để có thể giúp các bạn hiểu hơn về chi tiết về mô hình cũng như cách hoạt động của transformers trong NLP. + +### 3.1. Xây dựng mô hình transformer + +Kiến trúc tổng quan của transformers bao gồm 2 phần: **Encoder** và **Decoder**. + +**Nói thêm đoạn này: Autoregressive, encoder meaning and decoder meaning** + +
+ +
Hình 3.1.1. Kiến trúc tổng quan của mô hình Transformer - Paper
+
+ + +### 3.1.1. Input Encoding + +Với input embedding, ta cần xây dựng 2 khối chính là semantic embedding và positional embedding. + +```python +import torch +import torch.nn as nn +import torch.nn.functional as F +import math + +class SemanticEmbedding(nn.Module): + def __init__(self, vocab_size: int = 1000, + embedding_dim: int = 256): + super().__init__() + self.embedder = nn.Parameter(torch.randn(vocab_size, embedding_dim)) + + def forward(self, input_sequence: torch.Tensor): + return self.embedder[input_sequence] + + +class PositionalEncoding(nn.Module): + def __init__(self, d_model: int = 256, max_len: int = 1000): + super().__init__() + pe = torch.zeros(max_len, d_model) + position = torch.arange(0, max_len, dtype = torch.float).unsqueeze(1) + + div_term = torch.exp(torch.arange(0, d_model, 2).float()*(-math.log(10000.0)*2./d_model)) + + pe[:, ::2] = torch.sin(position*div_term) + pe[:, 1::2] = torch.cos(position*div_term) + + pe = pe.unsqueeze(0) + + self.register_buffer("pe", pe) + + def forward(self, x): + return self.pe[:, :x.size(1), :] +``` + +### 3.1.2. Multi-Head Attention + +Ở phần này, chúng ta sẽ code khối Multi-Head Attention. + +```python +# Trước tiên là khối self-attention + +class SelfAttention(nn.Module): + + def forward(self, query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + mask: torch.Tensor = None, + dropout: nn.Dropout = None): + + d_k = query.size(-1) + attention_score = F.softmax(torch.einsum("nqhd, nkhd -> nhqk", [query, key])/math.sqrt(d_k), -1) + + if mask is not None: + attention_score = attention_score.masked_fill(mask==0, 1e-9) + + if dropout is not None: + attention_score = dropout(attention_score) + + output = torch.einsum("nhqk, nvhd -> nvhd", [attention_score, value]) + return output + +# Tiếp theo là khối Multi-Head Attention + +class MultiHeadAttention(nn.Module): + def __init__(self, h, d_model, dropout_rate=0.1): + super().__init__() + assert d_model%h == 0 + + self.d_k = d_model//h + self.h = h + + self.query_projector = nn.Linear(d_model, d_model) + self.key_projector = nn.Linear(d_model, d_model) + self.value_projector = nn.Linear(d_model, d_model) + + self.output_linear = nn.Linear(d_model, d_model) + + self.attention = SelfAttention() + self.dropout = nn.Dropout(dropout_rate) + + def forward(self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + mask: torch.Tensor=None): + + batch_size = query.size(0) + + projected_query = self.query_projector(query).view(batch_size, -1, self.h, self.d_k) + projected_key = self.key_projector(key).view(batch_size, -1, self.h, self.d_k) + projected_value = self.value_projector(value).view(batch_size, -1, self.h, self.d_k) + + mha_output = self.attention(projected_query, projected_key, projected_value, mask, self.dropout).view(batch_size, -1, self.h*self.d_k) + output = self.output_linear(mha_output) + return mha_output +``` + +Sau khi code xong khối self-attention và self-attention, chúng ta sẽ test thử xem liệu output của các khối này có giống như chúng ta mong muốn không. + +```python +query = torch.randn((1, 10, 256)) + +key = torch.randn((1, 10, 256)) + +value = torch.randn((1, 15, 256)) + +multi_head_attention = MultiHeadAttention(4, 256) + +print(multi_head_attention(query, key, value).shape) +``` + +```bash +torch.Size([1, 15, 256]) +``` +Qua hình dạng của output, với output có dimension giống với kích thước của value tức là về shape nó đã đúng. Vì vậy, chúng ta sẽ qua bước tiếp theo là xây dựng bộ encoder và decoder. + +### 3.1.3. Encoder + +Khối encoder được xây dựng theo kiến trúc như bên trái hình 3.1.1. Khối này sẽ bao gồm 5 layers: Input Embedding, Multi-Head Attention, Add&Norm Layers, Feed Forward, Add&Norm Layer. + +```python +class TransformerEncoder(nn.Module): + def __init__(self, embedding_dim: int = 256, + num_heads: int = 4, + vocab_size: int = 10000, + max_seq_length: int = 100, + FF_dim: int = 1024): + super().__init__() + self.semantic_encoder = SemanticEmbedding(vocab_size, embedding_dim) + self.positional_encoder = PositionalEmbedding(d_model = embedding_dim, max_len=max_seq_length) + self.layer_norm_1 = nn.LayerNorm(embedding_dim) + + self.feed_forward = nn.Sequential(nn.Linear(embedding_dim, FF_dim), + nn.ReLU(), + nn.Linear(FF_dim, embedding_dim)) + + self.layer_norm_2 = nn.LayerNorm(embedding_dim) + + self.multi_head_attention = MultiHeadAttention(h = num_heads, d_model = embedding_dim) + + def forward(self, x: torch.Tensor): + x = self.semantic_encoder(x) + x = x + self.positional_encoder(x) + x = self.layer_norm_1( x + self.multi_head_attention(x, x, x)) + x = self.layer_norm_2(x + self.feed_forward(x)) + return x +``` + +Sau khi build xong khối encoder, chúng ta sẽ thử xem output shape có giống như chúng ta mong đợi không nhé. + +```python +if __name__ == "__main__": + + EMBEDDING_DIM = 256 + NUM_HEADS = 4 + VOCAB_SIZE = 10000 + MAX_SEQ_LENGTH = 100 + FF_DIM = 1024 + + encoder = TransformerEncoder( + embedding_dim=EMBEDDING_DIM, + num_heads=NUM_HEADS, + vocab_size=VOCAB_SIZE, + max_seq_length=MAX_SEQ_LENGTH, + FF_dim=FF_DIM, + ) + + sample = torch.arange(0, MAX_SEQ_LENGTH) + sample = (torch.rand((10, MAX_SEQ_LENGTH)) * (MAX_SEQ_LENGTH - 1)).long() + + encoded_information = encoder(sample) + + print(f"Sample: {sample}") + print(f"Encoded information: {encoded_information}") + print(f"Encoded information shape: {encoded_information.shape}") +``` + + +### 3.1.4. Decoder + + +### 3.2. Huấn luyện và chạy mô hình transformer cho classification task + +Cho classification task, chúng ta chỉ cần mỗi khối encoder của transformer vì đây là task không cần sinh ra output mới. Với classification task, chúng ta chỉ cần lấy feature được trích ra từ transformer encoder và đưa qua một lớp fully connected nữa để làm output cho classification. + +Ở ví dụ này, mình sẽ dùng data từ tập Disaster Tweets từ Kaggle. + +* **Step 1**: Chuẩn bị data + +```python +class CreateTextClassificationDataset(Dataset): + def __init__( + self, + df: pd.DataFrame, + vocabulary_size: int = None, + oov_token: str = "", + padding_type: str = "post", + truncating_type: str = "post", + max_len: int = None, + ): + + self.sentences = df["text"].values.tolist() + self.labels = df["target"].values + self.vocabulary_size = vocabulary_size + self.oov_token = oov_token + self.padding_type = padding_type + self.truncating_type = truncating_type + self.max_len = max_len + + # Create vectorised tokens + self._init_tokenizer() + self.vectorised_tokens = self._create_vectorised_tokens(self.sentences) + + + def _init_tokenizer(self) -> None: + self.tokenizer = Tokenizer( + num_words=self.vocabulary_size, oov_token=self.oov_token + ) + self.tokenizer.fit_on_texts(self.sentences) + + def _tokenize_texts(self, text: Union[str, List[str]]) -> List[int]: + tokens = self.tokenizer.texts_to_sequences(text) + return tokens + + def _vectorise_tokens(self, tokens: Union[List, List[int]]): + if self.max_len is None: + self.max_len = max([len(token) for token in tokens]) + + padded = pad_sequences( + tokens, + padding=self.padding_type, + truncating=self.truncating_type, + maxlen=self.max_len, + ) + return padded + + def _create_vectorised_tokens(self, sentences: List[str]) -> np.array: + tokenised = self._tokenize_texts(sentences) + vectorised = self._vectorise_tokens(tokenised) + return vectorised + + + def __len__(self): + return len(self.sentences) + + def __getitem__(self, idx): + return self.vectorised_tokens[idx], self.labels[idx] + +class CreateEvalDatasetForEvaluation(Dataset): + def __init__(self, eval_tokens: np.array, eval_targets: np.array): + self.eval_tokens = eval_tokens + self.eval_targets = eval_targets + + def __len__(self): + return self.eval_targets.shape[0] + + def __getitem__(self, idx): + return self.eval_tokens[idx], self.eval_targets[idx] +``` + +* **Step 2**: Kết hợp transformer encoder và classification head + +```python +encoder = TransformerEncoder( + embedding_dim=EMBEDDING_DIM, + num_heads=NUM_HEADS, + vocab_size=VOCAB_SIZE, + max_seq_length=MAX_LEN, + FF_dim=FF_DIM, +) + +model = nn.Sequential(encoder, + nn.Flatten(1), + nn.Linear(MAX_LEN*EMBEDDING_DIM, NUM_CLASSES)) + +model = model.to(DEVICE) + +optimizer = optim.Adam(model.parameters(), lr = LR, amsgrad=True) +loss_fn = nn.CrossEntropyLoss() +``` + +* **Step 3**: Pre-training (Configs and stuff) + +```python +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + +VOCAB_SIZE = 20000 +OOV_TOKEN = "" +PADDING_TYPE = "post" +TRUNCATING_TYPE = "post" +MAX_LEN = 33 +NUM_TRAINING_RATIO = 0.8 +EMBEDDING_DIM = 256 +NUM_HEADS = 4 +FF_DIM = 1024 +BATCH_SIZE = 32 +SHUFFLE = True +PIN_MEMORY = True +NUM_CLASSES = 2 +LR = 3e-4 +NUM_EPOCHS = 100 + +df = pd.read_csv("./nlp-getting-started/train.csv") +train_df = df.iloc[:int(NUM_TRAINING_RATIO*len(df))] +val_df = df.iloc[int(NUM_TRAINING_RATIO*len(df)):] + +train_ds = CreateTextClassificationDataset(train_df, vocabulary_size=VOCAB_SIZE, oov_token=OOV_TOKEN, padding_type=PADDING_TYPE, truncating_type=TRUNCATING_TYPE, + max_len=MAX_LEN) + +eval_tokens = train_ds._create_vectorised_tokens(val_df["text"].values.tolist()) +eval_targets = val_df["target"].values +eval_ds = CreateEvalDatasetForEvaluation(eval_tokens, eval_targets) + +train_loader = DataLoader(train_ds, batch_size = BATCH_SIZE, shuffle = SHUFFLE, pin_memory = PIN_MEMORY) +eval_loader = DataLoader(eval_ds, batch_size = BATCH_SIZE, shuffle=False, pin_memory=PIN_MEMORY) + +``` + +* **Step 4**: Training + +```python +def train_1_epoch(): + model.train() + loss_sum = 0 + for x, y in tqdm(train_loader): + x = x.long().to(DEVICE) + y = y.long().to(DEVICE) + logits = model(x) + loss = loss_fn(logits, y) + optimizer.zero_grad() + loss.backward() + optimizer.step() + loss_sum += loss.item() + + return loss_sum + + +@torch.inference_mode() +def evaluate_1_epoch(): + model.eval() + loss_sum = 0 + correct_samples = 0 + total_samples = 0 + for x, y in tqdm(eval_loader): + x = x.long().to(DEVICE) + y = y.long().to(DEVICE) + logits = model(x) + loss = loss_fn(logits, y) + loss_sum += loss.item() + correct_samples += len(logits.max(-1)[-1][logits.max(-1)[-1] == y]) + total_samples += logits.shape[0] + + return loss_sum, correct_samples / total_samples + + +def train(NUM_EPOCHS: int): + for epoch in range(1, NUM_EPOCHS + 1): + train_loss = train_1_epoch() + val_loss, val_acc = evaluate_1_epoch() + + torch.save(model.state_dict(), "last.pt") + + print( + f"Epoch: {epoch}, Train Loss: {train_loss}, Val Loss: {val_loss}, Val Acc: {val_acc}" + ) + +train(NUM_EPOCHS=NUM_EPOCHS) +``` + +```bash +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:14<00:00, 13.57it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 86.56it/s] +Epoch: 1, Train Loss: 142.10867008566856, Val Loss: 44.142126590013504, Val Acc: 0.5344714379514117 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:11<00:00, 16.63it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 86.23it/s] +Epoch: 2, Train Loss: 125.55668744444847, Val Loss: 29.014512717723846, Val Acc: 0.6868023637557452 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:12<00:00, 14.98it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 65.35it/s] +Epoch: 3, Train Loss: 112.70439422130585, Val Loss: 30.35796758532524, Val Acc: 0.6625082074852265 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:11<00:00, 16.17it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 83.36it/s] +Epoch: 4, Train Loss: 98.53315824270248, Val Loss: 28.567470982670784, Val Acc: 0.7150361129349967 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:11<00:00, 16.05it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 67.37it/s] +Epoch: 5, Train Loss: 87.58201515674591, Val Loss: 27.053630746901035, Val Acc: 0.7071569271175312 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:16<00:00, 11.38it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 52.34it/s] +Epoch: 6, Train Loss: 79.27146698534489, Val Loss: 29.58349298685789, Val Acc: 0.7038739330269206 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:16<00:00, 11.73it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 52.66it/s] +Epoch: 7, Train Loss: 71.90403440594673, Val Loss: 30.562148183584213, Val Acc: 0.7097833223900197 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:14<00:00, 13.26it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 87.81it/s] +Epoch: 8, Train Loss: 60.30563969910145, Val Loss: 32.01201945543289, Val Acc: 0.6841759684832567 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:12<00:00, 14.70it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 86.71it/s] +Epoch: 9, Train Loss: 56.01010598987341, Val Loss: 26.42184019088745, Val Acc: 0.7504924491135916 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:12<00:00, 15.25it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 52.39it/s] +Epoch: 10, Train Loss: 45.02339556068182, Val Loss: 30.82352478429675, Val Acc: 0.7032173342087984 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:16<00:00, 11.55it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 49.44it/s] +Epoch: 11, Train Loss: 37.822556521743536, Val Loss: 40.1178354145959, Val Acc: 0.7032173342087984 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:16<00:00, 11.56it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 52.37it/s] +Epoch: 12, Train Loss: 28.31721487827599, Val Loss: 52.655704917619005, Val Acc: 0.6001313197636244 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:14<00:00, 13.00it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 84.85it/s] +Epoch: 13, Train Loss: 28.7558862734586, Val Loss: 36.70165067911148, Val Acc: 0.7557452396585687 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:13<00:00, 14.66it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 52.40it/s] +Epoch: 14, Train Loss: 20.431814706884325, Val Loss: 30.56316629052162, Val Acc: 0.7544320420223244 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:15<00:00, 12.38it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 52.52it/s] +Epoch: 15, Train Loss: 16.69958796026185, Val Loss: 36.892881229519844, Val Acc: 0.7458962573867367 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:16<00:00, 11.93it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 86.53it/s] +Epoch: 16, Train Loss: 16.344361373223364, Val Loss: 36.886749021708965, Val Acc: 0.7314510833880499 +100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 191/191 [00:11<00:00, 16.28it/s] +100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 85.07it/s] +Epoch: 17, Train Loss: 14.930934912525117, Val Loss: 32.301914274692535, Val Acc: 0.7386736703873933 +``` + +Nhìn chung thì sau khoảng 17 epochs, model đạt 80% accuracy và có dấu hiệu bị overfit nhưng performance vẫn khá ổn. + +* **Step 5**: Inference + +Bước tiếp theo, mình sẽ thực hiện pipeline inference để chạy trực tiếp trên từng sample. +```python +sample = ["Heard about #earthquake is different cities, stay safe everyone."] + +vectorised_sample = train_ds._create_vectorised_tokens(sample) +vectorised_sample_torch = torch.from_numpy(vectorised_sample).long() +pred = model(vectorised_sample_torch).argmax(-1) +pred = "True" if pred.item() == 1 else "False" + +print(f"Prediction of {sample[0]} is {pred}") +``` + +```bash +Prediction of Heard about #earthquake is different cities, stay safe everyone. is True +``` + +### 3.3. Huấn luyện và chạy mô hình transformer để dịch tiếng Anh sang Tây Ban Nha + + +### 4. Lời kết + + + + +### References + +1\. [Attention is all you need - arXiv][paper] + +2\. [Positional embeddings in transformers EXPLAINED | Demystifying positional encodings - Youtube][positional embedding] + +3\. [Self Attention in Transformer Neural Networks (with Code!) - Youtube][self-attention] + + + + +[paper]: https://arxiv.org/pdf/1706.03762 +[positional embedding]: https://www.youtube.com/watch?v=1biZfFLPRSY&t=239s +[self-attention]: https://www.youtube.com/watch?v=QCJQG4DuHT0 diff --git a/_posts/2024-03-30-image_resize_explained.md b/_posts/2024-03-30-image_resize_explained.md new file mode 100644 index 00000000000..ed93c803112 --- /dev/null +++ b/_posts/2024-03-30-image_resize_explained.md @@ -0,0 +1,171 @@ +--- +title: "Giải thích về giải thuật resize ảnh" +mathjax: true +layout: post +--- + + +Xin chào các bạn! + +Trong bài post này, mình sẽ giải thích về một vài phương pháp resize ảnh thông dụng nhất trong lĩnh vực computer vision. Chúng ta thường dùng các thư viện có sẵn trong cv2, pytorch, tensorflow, PIL, ... để resize ảnh nhưng phương pháp nào phù hợp nhất với nhu cầu cũng như phương pháp nào nhanh nhất, việc này chỉ có thể biết được khi chúng ta hiểu được thuật toáncủa nó và đó cũng chính là mục đích của bài post này. + + +
+ +
Source: TechSmith
+
+ +### 1. Nearest Neighbour +Đây là thuật toán đơn giản nhất trong tất cả các thuật toán resize ảnh và cũng là thuật toán cho tốc độ xử lý nhanh nhất. Về cơ bản, phương pháp Nearest Neighbour sao chép giá trị pixel gần nhất với ảnh cần được resize. Giả dụ, nếu bạn scale một đường thẳng có 4 điểm thành đường thẳng có 9 điểm, thì điểm thứ 3 ở điểm thứ 9 sẽ có giá trị điểm 1 ở đường thẳng mà ban đầu có 4 điểm. Nguyên do là vì nếu bạn scale đường thẳng có 9 điểm về đường thẳng 4 điểm thì ta phải chia cho 2.25 và 3÷2.25 =1.33 làm tròn = 1. Nếu điểm đó nằm ở điểm .5 thì tùy ý các bạn lấy giá trị làm tròn lên hay tròn xuống nhé. +Một ví dụ trực quan khác là mình muốn scale 1 ảnh có shape là 2x2 lên thành 3x3 bằng phương pháp nearest neighbour. + +``` +A = [[1, 2], B = [[?, ?, ?], + [3, 4]] -> [?, ?, ?], + [?, ?, ?]] +``` + +Ở đây, ma trận B ở trục x gấp 3/2 A, và tương tự ở trục y. Vì vậy, nếu muốn tìm giá trị của B thì chỉ cần nhân giá trị vị trí của B với 2/3. +$B(0, 0) = A(round(0 *2/3), round(0 *2/3)) = A(0, 0) = 1$ +$B(1, 1) = A(round(1 *2/3), round(1 *2/3)) = A(0, 0) = 1$ +$B(2, 2) = A(round(2 *2/3), round(2 *2/3)) = A(1, 1) = 4$ +Vì vậy, ma trận B của chúng ta sẽ giống như vậy: +``` +B = [[1, 1, 2], + [1, 1, 2], + [3, 3, 4]] +``` + +Tuy đơn giản để implement và có tốc độ nhanh, tuy nhiên nó có những nhược điểm sau: ++ Ảnh bị vỡ, không mượt mà ++ Chất lượng ảnh bị giảm sút, đặc biệt khi resize ảnh lên kích thước lớn. + +#### Code + +```python +import numpy as np + +def nearest_neighbour(image: np.ndarray, new_shape: tuple): + ''' + For gray image only because this is a demo of nearest neighbour algorithm + If you want to do 3D version, just simply add another for loop for channel, the rest is the same. + Args: + image (np.ndarray): Original image + new_shape (tuple): target size + Returns: + Resized Image (np.ndarray) + ''' + assert isinstance(new_shape, tuple or list), \ + "Data type for new_shape must be tuple or list" + + currHeight, currWidth = image.shape + newHeight, newWidth = new_shape + emptyArr = np.zeros((newHeight, newWidth), dtype=np.uint8) + for i in range(newHeight): + for j in range(newWidth): + yPositionInOldImage = int((i/newHeight)*currHeight) + xPositionInOldImage = int((j/newWidth)*currWidth) + emptyArr[i][j] = image[yPositionInOldImage][xPositionInOldImage] + + return emptyArr + +A = np.array(range(1, 5)).reshape(2, 2).astype("uint8") +print(f"Image after resized: {nearest_neighbour(image = A, new_shape = (3, 3)})") +``` + +``` +>Image after resized: [[1 1 2] + [1 1 2] + [3 3 4]] +``` + + + +### 2. Bilinear Interpolation +Phương pháp này khắc phục nhược điểm ảnh bị vỡ nặng khi scale up ảnh của phương pháp nearest neighbours. Về cơ bản, phương pháp này cũng tìm vị trí x và y của ảnh cũ và tìm 4 điểm lân cận để tìm ra giá trị thay vì chỉ gán vào giá trị có vị trí gần nhất. + +
+ +
Source: Image Resampling Algorithms-Chathura Gunasekara
+
+ + + +Ta có công thức để suy ra giá trị của điểm ảnh trên hình được resize như trên. +Ví dụ minh họa, ta lấy ma trận A là ma trận ban đầu và ma trận B là ma trận được resized và $x'$ và $y'$ là điểm trên ma trận B mà ta muốn tìm ra giá trị, và $x, y$ là điểm ta suy ra từ vị trí $x', y'$ của ma trận B. Với điểm $Pixel(x, y)$ nằm trong 4 giá trị $Pixel(i, j), Pixel(i, j+1), Pixel(i+1, j), Pixel(i+1, j+1)$ và $a$ là khoảng cách từ $x$ đến $i$, và $b$ là khoảng cách từ $y$ đến $j$. Từ đó, ta có công thức để suy ra $Pixel(x', y')$ như sau: +$F(x', y')$ = $(1-a)(1-b)A(i, j)$ + $a(1-b)A(i+1, j)$ + $(1-a)bA(i, j+1)$ + $abA(i+1, j+1)$ + +Chúng ta lấy ví dụ trên để minh họa phương pháp này. Với + +$x'=1, y'=1$ ta có điểm $x = y =$ $2/3$ $*$ $1$ $= 0.667$. Vì vậy, ta suy ra được điểm này là điểm được bao bởi 4 điểm trong ma trận $A$ có vị trí lần lượt như sau $(0, 0), (0, 1), (1, 0), (1, 1)$. Và $a = b = 0.667$. Ta tính theo công thức để suy ra giá trị của điểm B(x, y) như sau: + +$B(1, 1) = (1-a)*(1-b)*A(0, 0) + (1-a) * b * A(1, 0) + a*(1-b)*A(0, 1) + a*b*A(1, 1)$ + +$B(1, 1) = (1-0.667)*(1-0.667)*1 + (1-0.667) * 0.667 * 3 + 0.667*(1-0.667)*2 + 0.667*0.667*4 = 2.989$ + +Một ví dụ khác với $x'=2, y'=2$, ta có điểm $x = y =$ $2/3$ $*$ $2$ $= 1.333$. Vì vậy, ta suy ra được điểm này là điểm được bao bởi 4 điểm trong ma trận $A$ có vị trí lần lượt như sau $(1, 1), (2, 1), (1, 2), (2, 2)$. Với điểm nào nằm ngoài điểm biên thì ta thay giá trị điểm đó trùng với điểm biên luôn nha .Và $a = b = 0.333$. Ta tính theo công thức để suy ra giá trị của điểm B(x, y) như sau: + +$B(1, 1) = (1-a)*(1-b)*A(1, 1) + (1-a) * b * A(1, 1) + a*(1-b)*A(1, 1) + a*b*A(1, 1)$ + +$B(1, 1) = (1-0.333)*(1-0.333)*4 + (1-0.333) * 0.333 * 4 + 0.333*(1-0.333)*4 + 0.333*0.333*4 = 4$ + + + +Và nếu ta tính tương tự như vậy, ta sẽ có kết quả ma trận B như sau: + +``` +B = [[1. , 1.666, 2.], + [2.333, 2.989, 3.33], + [3. , 3.667, 4.]] +``` +Như có thể thấy thì ma trận B từ phương pháp Bilinear Interpolation cho kết quả nhìn mượt hơn phương pháp Nearest Neighbours. Tuy nhiên phương pháp cũng có nhược điểm là thời gian tính toán lâu hơn so với phương pháp trên. + +#### Code + +```python +import numpy as np + +def bilinear_interpolation(image, new_width, new_height): + height, width = image.shape + + x_scale_factor = width/new_width + y_scale_factor = height/new_height + + result = np.zeros((new_height, new_width), dtype = np.float32) + + for y in range(new_height): + for x in range(new_width): + src_x = x*x_scale_factor + src_y = y*y_scale_factor + + x1 = int(src_x) # round down + y1 = int(src_y) # round down + x2 = min(x1+1, width-1) + y2 = min(y1+1, height-1) + + alpha = src_x - x1 + beta = src_y - y1 + + result[y, x] = (1-alpha)*(1-beta)*image[y1, x1] + alpha*(1-beta)*image[y1, x2] + \ + (1-alpha)*beta*image[y2, x1] + alpha*beta*image[y2, x2] + return result + +A = np.array(range(1, 5)).reshape(2, 2).astype("float32") +print(f"Image after resized: {bilinear_interpolation(image = A, new_width = 3, new_height = 3)}") +``` + +``` +Image after resized: [[1. 1.6666666 2. ] + [2.3333333 3. 3.3333333] + [3. 3.6666667 4. ]] +``` + + +### 3. Bicubic Interpolation + + + + +### References + +1. \ No newline at end of file diff --git a/_posts/2024-03-30-vgg_explained.md b/_posts/2024-03-30-vgg_explained.md new file mode 100644 index 00000000000..08bc9914b00 --- /dev/null +++ b/_posts/2024-03-30-vgg_explained.md @@ -0,0 +1,298 @@ +--- +title: "Giải thích và code paper Very Deep Convolutional Networks for Large-Scale Image Recognition" +layout: post +--- + + +Xin chào các bạn! + +Trong bài post này mình sẽ giới thiệu về kiến trúc VGG, một trong những kiến trúc lâu đời nhất của mạng tích chập. Thật tình cờ, vào thời điểm viết bài blog này cũng là tròn 10 năm ngày ra mắt của mô hình này. Vậy, VGG có gì đặc biệt và có ảnh hưởng đến cách xây dựng các mô hình CNNs sau này như thế nào, chúng ta cùng khám phá nhé. + + +### 1. Một chút background về các tiền bối của VGG +Vào thời điểm 10 năm trước, có 2 mô hình CNNs rất nổi trước khi VGG được ra mắt đó là AlexNet (2012) được published trong paper "_ImageNet Classification with Deep Convolutional Neural Networks_" và LeNet của YanLeCun. Mô hình LeNet của YanLeCun là mô hình đầu tiên áp dụng thuật toán convolution vào mạng neural networks. Còn về AlexNet, mô hình này cực kì nổi tiếng mà hầu như ai trong lĩnh vực AI này cũng biết, nó đặc biệt ở chỗ dùng hàm ReLU activation function, sử dụng Dropout và Local Response Normalisation, và training với nhiều GPUs. + +Tuy nhiên, nếu nhìn qua về kiến trúc của cả 2 mạng này, ta sẽ thấy nó khá là nông với chỉ từ 5 đến 6 layers. Một điều nữa là cách sử dụng convolutional layers ở các mạng này là dùng kernel size và stride có kích cỡ lớn ở những layers đầu để thu nhỏ receptive field. + + +
+Kiến trúc của LeNet-5 và AlexNet +
Hình 1.1. Kiến trúc của LeNet-5 và AlexNet
+
+ + + +Như có thể thấy ở trên, mạng AlexNet có nhiều convolutional layers hơn so với LeNet tuy nhiên, AlexNet chỉ dừng lại ở 5 convolution layers và 3 fully connected layers. + +### 2. Cấu trúc của VGG Network +Mạng VGG khác với các mô hình đi trước ở 2 điểm chính đó là sử dụng kernel size = 3 xuyên suốt toàn bộ kiến trúc và độ sâu của mô hình được tăng lên 16-19 layers thay vì chỉ vài layers như AlexNet và LeNet trước đó. + +Trong paper của mình, tác giả VGG đề xuất 6 options cho VGG đó là A, A-LRN, B, C, D, và E. Với từng options sẽ có thông số cụ thể như sau: + +* A: 11 layers với với toàn bộ kernel size là 3x3. +* A-LRN: Tương tự như A nhưng sử dụng Local Response Normalisation sau conv layer đầu tiên. +* B: 13 layers với toàn bộ kernel size là 3x3. +* C: 16 layers với toàn bộ kernel-size là 3x3 và 1x1 ở 3 khối cuối cùng. +* D: 16 layers với toàn bộ kernel-size là 3x3. +* E: 19 layers với toàn bộ kernel-size là 3x3. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConvNet Configuration
AA-LRNBCDE
11 weight layers11 weight layers13 weight layers16 weight layers16 weight layers19 weight layers
input (224x224 RGB image)
Conv3-64Conv3-64
+ LRN
Conv3-64
+ Conv3-64
Conv3-64
+ Conv3-64
Conv3-64
+ Conv3-64
Conv3-64
+ Conv3-64
MaxPool
Conv3-128Conv3-128Conv3-128
+ Conv3-128
Conv3-128
+ Conv3-128
Conv3-128
+ Conv3-128
Conv3-128
+ Conv3-128
MaxPool
Conv3-256
+ Conv3-256
Conv3-256
+ Conv3-256
Conv3-256
+ Conv3-256
Conv3-256
+ Conv3-256
Conv1-256
Conv3-256
+ Conv3-256
Conv3-256
Conv3-256
+ Conv3-256
Conv3-256
+ Conv3-256
MaxPool
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
Conv1-512
Conv3-512
+ Conv3-512
Conv3-512
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
MaxPool
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
Conv1-512
Conv3-512
+ Conv3-512
Conv3-512
Conv3-512
+ Conv3-512
Conv3-512
+ Conv3-512
MaxPool
FC-4096
FC-4096
FC-1000
Softmax
+ + + + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 2.1. Kiến trúc của AlexNet và VGG-16 (một option trong kiến trúc VGG)
+
+ + +So sánh giữa 2 kiến trúc như trên hình thì có thể thấy được rằng kiến trúc VGG sâu hơn hẳn so với AlexNet. + +Với kiến trúc sử dụng full conv 3x3 và 16 layers, tác giả Karen Simonyan & Andrew Zisserman đã chứng minh được độ hiệu quả với việc giảnh giải nhất của cuộc thi ILSVRC 2014 cho task classification và giải nhi cho task localisation. + +### 3. Coding time +Giờ mình sẽ build kiến trúc này với Pytorch để chúng ta có thể nắm rõ hơn kiến trúc này nhé. +```python +import torch +from torch import nn + +torch.set_grad_enabled(False) + +# Configuration for each option +cfg = { + 'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'], + 'A-LRN': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'], + 'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'], + 'C': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'], + 'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'], + 'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'], +} + + +class VGG(nn.Module): + def __init__(self, vgg_name, num_classes): + super().__init__() + if vgg_name not in cfg: + raise "Architecture not included, please try again" + + self.layer_names = cfg[vgg_name] + layers = [nn.Conv2d(3, self.layer_names[0], kernel_size=3, padding = 1)] + if "LRN" in vgg_name: + layers += [nn.LocalResponseNorm(5)] + + layers += [nn.ReLU()] + + last_num_layer = self.layer_names[0] + for ith, layer_name in enumerate(self.layer_names[1:]): + if isinstance(layer_name, int): + if vgg_name == 'C' and ith >= 6 and self.layer_names[ith+2] == 'M': + layers += [nn.Conv2d(last_num_layer, layer_name, kernel_size=1, stride=1, padding = 0)] + else: + layers += [nn.Conv2d(last_num_layer, layer_name, kernel_size=3, stride=1, padding = 1)] + + layers += [nn.ReLU()] + last_num_layer = layer_name + else: + layers.append(nn.MaxPool2d(2, 2)) + + self.layers = nn.Sequential(*layers) + self.global_avg_pool = nn.AdaptiveAvgPool2d(1) + self.fc = nn.Linear(last_num_layer, num_classes) + + def forward(self, x): + x = self.layers(x) + x = self.global_avg_pool(x) + x = x.view(x.shape[0], -1) + return self.fc(x) + + + +if __name__ == "__main__": + modelA = VGG("A", 10) + modelALRN = VGG("A-LRN", 10) + modelB = VGG('B', 10) + modelC = VGG("C", 10) + modelD = VGG("D", 10) + modelE = VGG('E', 10) + + dummy = torch.randn((1, 3, 224, 224)) + print(f"Model A output shape: {modelA(dummy).shape}") + print(f"Model A-LRN output shape: {modelALRN(dummy).shape}") + print(f"Model B output shape: {modelB(dummy).shape}") + print(f"Model C output shape: {modelC(dummy).shape}") + print(f"Model D output shape: {modelD(dummy).shape}") + print(f"Model E output shape: {modelE(dummy).shape}") +``` + +Output của mô hình +``` +> Model A output shape: torch.Size([1, 10]) +> Model A-LRN output shape: torch.Size([1, 10]) +> Model B output shape: torch.Size([1, 10]) +> Model C output shape: torch.Size([1, 10]) +> Model D output shape: torch.Size([1, 10]) +> Model E output shape: torch.Size([1, 10]) +``` + +### 4. Thảo luận (Optional) +Trong các kiến trúc của AlexNet hay LeNet, tại sao họ không làm kiến trúc sâu hơn? Lý do chính bởi vì họ dùng kernel size và stride lớn nên đến khoảng layer 5 và 6 thì số lượng features đã rất nhỏ nên họ đã không thể cho thêm nhiều layers để extract features hơn nữa. + +Tại sao tác giả của VGG dùng 3x3 mà không phải là 5x5, 2x2, 4x4? Lý do chính họ sử dụng kernel size 3x3 là vì đây là kernel size nhỏ nhất có thể bắt được toàn bộ vùng xung quanh của một pixels (trái/phải, trên/dưới, và chính giữa). Hơn nữa việc dùng 2 kernel 3x3 tương tự với việc dùng conv 5x5 với ít số lượng parameters hơn, nhưng **khối lượng tính toán sẽ nhiều hơn** (tác giả không nhắc đến điều này trong paper). + +### 5. Kết luận +Mô hình VGG đã làm thay đổi cách các AI researchers/scientists thiết kế các kiến trúc CNN, với việc mô hình hiện đại ngày nay đa số dùng kernel size 3x3, và có độ sâu lớn hơn rất nhiều so với các mô hình ra mắt trước VGG. Vì vậy, ta có thể nói VGG là một pioneer model. + +Cảm ơn bạn đã đọc đến cuối bài, hãy ủng hộ mình bằng cách tiếp tục theo dõi và feedback nếu thấy chỗ nào chưa hợp lý nhé. + +### References +1\. [Very Deep Convolutional Networks for Large-Scale Image][Very Deep Convolutional Networks for Large-Scale Image] +2\. [The Convolutional Neural Network - Theory and Implementation of LeNet-5 and AlexNet][The Convolutional Neural Network - Theory and Implementation of LeNet-5 and AlexNet] + + + +[The Convolutional Neural Network - Theory and Implementation of LeNet-5 and AlexNet]: https://pabloinsente.github.io/the-convolutional-network +[Very Deep Convolutional Networks for Large-Scale Image]: https://arxiv.org/pdf/1409.1556.pdf \ No newline at end of file diff --git a/_posts/2024-04-05-ParamCounting.md b/_posts/2024-04-05-ParamCounting.md new file mode 100644 index 00000000000..11299c0b9da --- /dev/null +++ b/_posts/2024-04-05-ParamCounting.md @@ -0,0 +1,195 @@ +--- +title: "Hướng dẫn tính số lượng params trong các mạng Neural Networks - Phần 1" +mathjax: true +layout: post +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ hướng dẫn tính thủ công số lượng parameters trong các mạng tích chập. + + +
+ +
Source: abcvector
+
+ + +### 1. Tại sao việc tính tay số lượng parameters lại quan trọng ? +Việc biết tính số lượng parameters của một mô hình sẽ giúp chúng ta có được cái nhìn rõ và tổng quan hơn về mô hình trước khi ta bắt tay xây dựng hoặc deploy nó và việc này sẽ tiết kiệm được kha khá thời gian. Giả sử, khi deploy lên mobile, web, hay edge devices, chúng ta cần tìm mô hình nào có số lượng params ít nhất (càng ít params thì file weights càng nhẹ). Việc đầu tiên các bạn thường làm sẽ là tìm kiếm papers của các model và đọc thử nếu thấy hợp tiêu chí thì sẽ build hoặc clone repo về. Và khi biết được cách tính số lượng params rồi thì ta chỉ cần đọc lướt qua về phần kiến trúc là đã biết được liệu mô hình này liệu có đủ nhẹ để deploy không thay vì phải build và dùng các tools hoặc frameworks để kiểm tra. Ngoài ra, việc biết cách tính số lượng params một cách thủ công còn giúp chúng ta nắm rõ được papers hơn. + +### 2. Cách tính số lượng params +Mình sẽ hướng dẫn các bạn tính số lượng params trong các mạng cơ bản nhất để có nền tảng để tính các mạng phức tạp hơn. Thực chất, các mạng phức tạp cũng chỉ cấu thành từ những mạng cơ bản. Nếu tính được các mạng cơ bản thì mạng nào chúng ta cũng có thể tính được. + +#### 2.1. Cách tính số lượng params cho mạng linear + +
+ +
Source: Aldo Zaimi
+
+ +Theo công thức, mạng linear được tính như sau: + +$$Z = Weight^T \cdot X + bias $$ + +Với + +$$ X \in R^{D_1*N} $$ + +$$ Weight \in R^{D_1*D_2}$$ + +$$bias \in R^{D_2*1}$$ + +$$N:\text{Batch Size}$$ + +$$D_2: \text{Dimension của layer hiện tại (số lượng neurons trong layer đó)}$$ + +$$D_1: \text{Dimension của layer trước (số lượng neurons trong layer trước)}$$ + +Vì trong layer này có tổng cộng $$D_1*D_2$$ weights và $$D_2$$ của bias, nên tổng cộng có số lượng weights là: + +$$\text{Number of layer's parameters}= D_1*D_2 + D_2$$ + +Trong mạng này, có tổng cộng là 2 layers: layer giữa và layer cuối. Layer đầu ta không tính vì đó là input và trong ví dụ này dimension của input là 2. Vì vậy, ta có cách tính số lượng parameters của mạng này như sau: + +$$\text{Total parameters} = \text{Number of layer 1's parameters} + \text{Number of layer 2's parameters}$$ + +$$-> \text{Total parameters} = (5*2 + 5) + (1*5 + 1) = 15 + 6 = 21$$ + +Ta hãy dùng Tensorflow xây dựng và kiểm tra thử liệu số lượng parameters chúng ta tính có đúng không nhé + +```python +import tensorflow as tf +from tensorflow import keras +from keras import layers + +model = keras.Sequential([ + layers.Input(shape=(2, )), + layers.Dense(5), + layers.Dense(1) +]) + + +print(model.summary()) +``` + + +``` +Model: "sequential" +_________________________________________________________________ + Layer (type) Output Shape Param # +================================================================= + dense (Dense) (None, 5) 15 + + dense_1 (Dense) (None, 1) 6 + +================================================================= +Total params: 21 +Trainable params: 21 +Non-trainable params: 0 +_________________________________________________________________ +``` + +Correctamundo!! +#### 2.2. Cách tính số lượng params cho mạng convolution + +
+ +
Source: Emmanuel Byrd
+
+ + +Với mạng convolution, chúng ta sẽ có cách tính hơi khác do cách hoạt động của mạng này khác với mạng linear ở trên. Tuy nhiên, về mặt bản chất, cách tính của chúng tương tự nhau. + +Trong Pytorch, 3 thông số quan trọng bạn phải set cho một layer convolution là **channel_in** và **channel_out**, và **kernel_size**. Đối với Tensorflow chỉ cần 2 thông số out_channels và kernel_size do in_channels sẽ được tự động tìm ra. + +```python +conv_layer = nn.Conv2d(in_channels=3, out_channels=16, kernel_size = 3) +``` + +Có lẽ bạn thắc mắc tại sao 3 thông số này quan trọng? Vì nó sẽ quyết định số lượng parameters của lớp conv đó. + +Mối quan hệ giữa input và output của convolution layer có công thức như sau: + +$$out(N_i, C_{out_j}) = bias(C_{out_j}) + \sum_{k=0}^{C_{in}-1} weight(C_{out}, k)*input(N_i, k)$$ + + +Đoạn code dưới đây minh họa giải thuật tích chập của 1 kernel cho một vùng của ảnh theo công thức trên: + +```python +# Đây là đoạn code để giúp các bạn hiểu hơn về cách hoạt động của một kernel +import numpy as np + +# Giả sử chúng ta có input có kích thước 3x3x3, và kernel 3x3 +input = np.ones((3, 3, 3), dtype=np.float32) +kernel = np.random.randn((3, 3, 3)) +bias = np.random.randn(1) + +# output của nó sẽ như sau: +output = (kernel*input).sum() + bias +``` + +Nếu bạn set output channels = 16 như đoạn code trên thì PyTorch/Tensorflow sẽ tạo ra 16 kernels cùng kích thước nhưng khác params và mỗi kernel sẽ quét qua input và output ra 1 layer. + +Như ví dụ minh họa trên, với mỗi một kernel ta sẽ có số parameters như sau $$\text{kernel width} * \text{kernel height} * \text{input channels} + 1 (bias)$$. Và nếu ta đặt số lượng kernel là $$C_{out}$$ thì ta sẽ có tổng số lượng parameters trong một convolution layer là: + +$$\text{Number parameters of a convolution layer} = \text{kernel width}*\text{kernel height}*\text{input channels}* \text{output channels} + \text{output channels}$$ + + +Ví dụ, nếu ta có một tấm ảnh có kích thước $$Width * Height * 3$$ và ta set cho lớp conv có 16 channels out và kernel size = 3 thì ta sẽ có số lượng parameters như sau: + + +$$\text{Number of parameters} = 3*3*3*16 + 16 = 448$$ + +Ta sẽ kiểm tra thử bằng framework Tensorflow nhé, do Pytorch ở thời điểm chưa hỗ trợ nên mình dùng tạm Tensorflow vậy. + +```python +import tensorflow as tf +from tensorflow import keras +from keras import layers + +model = keras.Sequential([ + # Mình set tạm width=height=224 vì nó không ảnh hưởng tới số lượng params + layers.Input(shape=(224, 224, 3)), + layers.Conv2D(16, kernel_size=3), +]) + + +print(model.summary()) +``` + +``` +Model: "sequential" +_________________________________________________________________ + Layer (type) Output Shape Param # +================================================================= + conv2d (Conv2D) (None, 222, 222, 16) 448 + +================================================================= +Total params: 448 +Trainable params: 448 +Non-trainable params: 0 +_________________________________________________________________ +``` +Correctamundo!! + +Lưu ý: Kích thước dài và rộng của input sẽ không ảnh hưởng đến số lượng params, nó chỉ ảnh hưởng đến số phép tính cần thực hiện. + + +Vì thế, nếu bạn set càng nhiều output channels thì số parameters sẽ càng nhiều. Và nếu các bạn đọc kiến trúc của các mô hình họ thiết kế **càng nhiều output layers thì số lượng parameters của họ** sẽ càng lớn. + +Tuy nhiên, trừ một trường hợp họ sử dụng Depth-wise conv (đây là trường hợp đặc biệt do họ chỉ dùng 1 kernel giống nhau quét qua input **out_channels** lần thay vì dùng **out_channels** kernel khác nhau quét qua input). Mình sẽ giải thích rõ kiến trúc này ở phần sau nha. + + +### 3. Kết luận +Qua hai ví dụ trên, mình đã hướng dẫn các bạn tính số lượng parameters của hai mạng cơ bản nhất trong neural networks. Hai mạng này đã tạo nên những kiến trúc khổng lồ và hiện đại như ChatGPT, Diffusion, GANs, BERTs nên nếu biết các tính chính xác 2 mạng trên thì bạn có thể tính hoặc nhẩm được 90% các mạng trên thị trường rùi nhé. Còn 10% kia mình sẽ cover ở phần sau nhe. + +Nếu bạn thích bài viết thì hãy ủng hộ mình bằng cách tiếp tục theo dõi và phản hồi lại nếu có gì bạn thấy chưa hợp logic hay không hiểu nhé. + + +### References +1. [Pytorch documentation - Linear](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html) + +2. [Pytorch documentation - Conv2d](https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html) + + diff --git a/_posts/2024-04-08-image_filters.md b/_posts/2024-04-08-image_filters.md new file mode 100644 index 00000000000..e26a3574c57 --- /dev/null +++ b/_posts/2024-04-08-image_filters.md @@ -0,0 +1,458 @@ +--- +title: "Giải thích và implement các bộ lọc trong xử lý ảnh - Phần 1" +mathjax: true +layout: post +--- + +Xin chào các bạn, + +Trong phần 1 của series bộ lọc ảnh, mình sẽ giới thiệu về bộ lọc ảnh, ứng dụng, và một vài bộ lọc ảnh cơ bản nhất trong lĩnh vực xử lý ảnh, thị giác máy. + + + +### 1. Giới thiệu + +Giải thích một cách ngắn gọn thì bộ lọc ảnh là thứ mà sẽ giúp chúng ta **biến tấm ảnh gốc thành tấm ảnh mà chúng ta muốn thấy**. + +
+ +
Source: thegioididong - Polish: Ứng dụng chỉnh sửa, hiệu ứng độc đáo, bộ lọc ảnh đẹp, tách ảnh ra khỏi nền bằng AI
+
+ +Nếu các bạn dùng Tiktok, Instagram, ... thì các bạn sẽ thấy mục **filters**, tức là bộ lọc, và việc sử dụng filters khác nhau sẽ cho những hiệu ứng khác nhau trên tấm ảnh. Nếu bạn muốn có tai thỏ hoặc những chú cá dễ thương trong khung hình thì bộ lọc ảnh sẽ giúp bạn làm điều đó. + +Ngoài ra, bộ lọc có thể làm được nhiều thứ vi diệu hơn như thay đổi khuôn mặt, thay đổi màu sắc, khử nhiễu, phân vùng bức ảnh, ... Và những điều này là sự áp dụng và kết hợp nhiều bộ lọc khác nhau. + +
+ +
Source: War Room - The Deep Fakes are coming
+
+ + +### 2. Bộ lọc hoạt động như thế nào ? +Có nhiều loại bộ lọc và mỗi bộ lọc ảnh sẽ được thiết kế dựa trên một thuật toán cụ thể, thuật toán này sẽ xác định cách thức biến đổi dữ liệu hình ảnh. Vì vậy, tất cả bộ lọc sẽ không hoạt động một cách giống nhau và có một công thức tổng quát. + +Một vài bộ lọc cơ bản hoạt động theo kiểu tích chập (convolution) và một vài bộ lọc phức tạp hơn thì hoạt động theo kiểu thuật toán (algorithm). Tuy nhiên, những bộ lọc phức tạp đều có thể được xấp xỉ bằng các bộ lọc cơ bản hoạt động cơ bản theo kiểu tích chập. Mình sẽ chứng minh phần này ở các phần tiếp theo. + + +### 3. Một vài bộ lọc cơ bản thông dụng +Mình sẽ giới thiệu một vài bộ lọc cơ bản và thông dụng nhất để các bạn nắm được cách hoạt động, công dụng, cũng như điểm mạnh và điểm yếu của nó nhé. Ngoài ra mình cũng sẽ code các bộ lọc này from scratch để các bạn có thể nắm tốt nhất cách hoạt động của các bộ lọc này. + + +#### 3.1. Bộ lọc trung bình (Average Filter - Box Filter) +Công dụng của bộ lọc này là làm mờ và giúp ảnh nhìn "mượt" hơn. Có lẽ bạn thắc mắc, tại sao chúng ta muốn làm mờ ảnh? + +Làm mờ ảnh có nhiều công dụng như khử nhiễu, không để lộ khuôn mặt, ... +Nếu các bạn nào xem các vụ hiện trường án mạng hay tai nạn, thì khuôn mặt hoặc hình nhạy cảm sẽ được blur đi và nhìn như hình dưới: + +
+ +
Source: Cloudinary - Automatically Add Blur Faces Effect To Images +
+
+ +Bộ lọc này có cách hoạt động cực kì đơn giản. Như tên gọi của nó, average filter lấy tất cả pixels trong một vùng cố định (users sẽ chọn kích thước của vùng) và output ra giá trị trung bình của vùng đó. + +**Code** + +```python +import cv2 +import numpy as np + +def averageFilter(image: np.ndarray, kernelSize: int = 3, keepShape: bool = True): + # Calculate the padding size based on the kernel size + padding = kernelSize // 2 + + # Add padding to the image + paddedImage = cv2.copyMakeBorder(image, padding, padding, padding, padding, cv2.BORDER_CONSTANT) + + # Initialize an empty result image + result = np.zeros_like(image) + + # Apply the average filter + for i in range(padding, paddedImage.shape[0] - padding): + for j in range(padding, paddedImage.shape[1] - padding): + # Extract the region of interest (ROI) from the padded image + roi = paddedImage[i - padding:i + padding + 1, j - padding:j + padding + 1] + + # Calculate the average value of the ROI + averageValue = np.mean(roi) + + # Assign the average value to the corresponding pixel in the result image + result[i - padding, j - padding] = averageValue + + # If keepShape is False, remove the padding and resize the result image to match the input image shape + if not keepShape: + result = result[padding:result.shape[0] - padding, padding:result.shape[1] - padding] + + return result + +# Create a test image +image = np.arange(1, 26).reshape(5, 5).astype("float32") + +# Apply the average filter to the image +filteredImage = averageFilter(image) + + +# Print the filtered image +print(f"Original image: {image}") +print(f"After average filtering: {filteredImage}") +``` + +``` +Original image: + [[ 1. 2. 3. 4. 5.] + [ 6. 7. 8. 9. 10.] + [11. 12. 13. 14. 15.] + [16. 17. 18. 19. 20.] + [21. 22. 23. 24. 25.]] + +After average filtering: + [[ 1.7777778 3. 3.6666667 4.3333335 3.1111112] + [ 4.3333335 7. 8. 9. 6.3333335] + [ 7.6666665 12. 13. 14. 9.666667 ] + [11. 17. 18. 19. 13. ] + [ 8.444445 13. 13.666667 14.333333 9.777778 ]] +``` + + +#### 3.2. Bộ lọc trung vị (Median Filter) +Công dụng chính của bộ lọc này là để giải quyết các salt-and-pepper noise. Loại nhiễu này thường xuất hiện do lỗi truyền dữ liệu, lỗi ô nhớ hoặc lỗi do chuyển đổi tín hiệu analogue sang tín hiệu digital. + +
+ +
Source: Avajsc - Cách làm tivi hết nhiễu và một số mẹo vặt cực kỳ đơn giản với tivi nhà bạn +
+
+ +Nếu các bạn còn dùng Tivi ăng ten thì sẽ nhận ra loại nhiễu này, hehe. + + +Vì các loại nhiễu này chỉ gồm 2 giá trị 0 và 255, nên nếu ta dùng bộ lọc trung vị thì có khả năng cao có thể loại ra được chúng. Nói có khả năng cao vì bộ lọc trung vị vẫn sẽ để lọt salt-and-pepper noise, nếu noise càng dày và đặc thì sẽ khó hơn để lọc ra và ngược lại. Vì giá trị 0-255 của salt-and-pepper noise sẽ nằm ở 2 biên của một vùng ảnh nên bộ lọc trung vị sẽ giúp chúng ta lọc ra được những điểm noise này, tuy nhiên ảnh của chúng ta sẽ mất độ chi tiết khi dùng bộ lọc này. + +Các giá trị cần set cho bộ lọc trung vị là **kích thước kernel**. Nếu bạn set kernel 3x3 thì nó sẽ lướt qua từng vùng 3x3 trong ảnh là lọc ra trung vị của điểm đó và điền nó vào tấm ảnh mới. + +
+ +
Source: Matlab +
+
+ +Giải thuật median filter được trình bày như sau: ++ **Step 1**: Quét kernel qua một vùng của tấm ảnh ++ **Step 2**: Sắp xếp các giá trị thấp dần/cao dần ++ **Step 3**: Chọn giá trị chính giữa và thay vào ảnh mới + + +**Code** +```python +import cv2 +import numpy as np + +def medianFilter(image: np.ndarray, kernelSize: int = 3, keepShape: bool = True): + # Calculate the padding size based on the kernel size + padding = kernelSize // 2 + + # Add 0 padding to the image + paddedImage = cv2.copyMakeBorder(image, padding, padding, padding, padding, cv2.BORDER_CONSTANT) + + # Initialize an empty result image + result = np.zeros_like(image) + + # Apply the average filter + for i in range(padding, paddedImage.shape[0] - padding): + for j in range(padding, paddedImage.shape[1] - padding): + # Extract the region of interest (ROI) from the padded image + roi = paddedImage[i - padding:i + padding + 1, j - padding:j + padding + 1] + + # Calculate the median value of the ROI + medianValue = np.median(roi) + + # Assign the average value to the corresponding pixel in the result image + result[i - padding, j - padding] = medianValue + + # If keepShape is False, remove the padding and resize the result image to match the input image shape + if not keepShape: + result = result[padding:result.shape[0] - padding, padding:result.shape[1] - padding] + + return result + +# Create a test image +image = np.arange(1, 26).reshape(5, 5).astype("float32") + +# Apply the average filter to the image +filteredImage = medianFilter(image) + + +# Print the filtered image +print(f"Original image: \n {image}") +print(f"After median filtering: \n {filteredImage}") +``` + + +``` +Original image: + [[ 1. 2. 3. 4. 5.] + [ 6. 7. 8. 9. 10.] + [11. 12. 13. 14. 15.] + [16. 17. 18. 19. 20.] + [21. 22. 23. 24. 25.]] + +After median filtering: + [[ 0. 2. 3. 4. 0.] + [ 2. 7. 8. 9. 5.] + [ 7. 12. 13. 14. 10.] + [12. 17. 18. 19. 15.] + [ 0. 17. 18. 19. 0.]] +``` + +#### 3.3. Bộ lọc Gauss (Gauss Filter) +Bộ lọc Gauss filter về chức năng tương tự như average filter. Tuy nhiên, điểm khác nhau là bộ lọc này sử dụng gán các trọng số cho các pixels xung quanh theo phân phối Gaussian thay vì gán các trọng số cho các pixels xung quanh bằng nhau như bộ lọc trung bình. + +
+ +
Source: Robot Academy - Introducing Kernels +
+
+ +Gauss kernel có 2 đặc tính là: ++ Đối xứng ++ Trọng số của pixels giảm dần nếu vị trí càng xa vị trí trung tâm (đối với average filter thì trọng số bằng nhau dù gần hay xa trung tâm) ++ Ảnh kết quả sẽ mịn hơn so với average filter nếu ảnh ban đầu bị nhiễu nặng, hoặc có nhiều góc cạnh. + +Để sử dụng Gauss Filter, chúng ta cần set 3 giá trị: **kernel_size**, **var_x**, **var_y**. + +**Code** + +Đầu tiên chúng ta sẽ viết function tạo Gauss filter, đoạn code dưới đây sẽ giúp chúng ta thực hiện điều đó. + +```python +def gkernel(l=3, sig=2): + + ax = np.linspace(-(l - 1) / 2., (l - 1) / 2., l) + xx, yy = np.meshgrid(ax, ax) + + kernel = np.exp(-0.5 * (np.square(xx) + np.square(yy)) / np.square(sig)) + + return kernel / np.sum(kernel) +``` + +Tiếp theo, ta chỉ cần viết một function giống như average filter, nhưng lần này thay vì lấy trung bình, ta sẽ lấy tổng của các tích của vùng ROI với bộ lọc Gauss. + +```python + +def gaussianFilter(image: np.ndarray, + kernelSize: int = 3, + sigma: float = 1.0, + keepShape: bool = True): + # Calculate the padding size based on the kernel size + padding = kernelSize // 2 + + # Add padding to the image + paddedImage = cv2.copyMakeBorder(image, padding, padding, padding, padding, cv2.BORDER_CONSTANT) + + # Create a Gaussian kernel + kernel = gkernel(l=kernelSize, sig=1) + + # Initialize an empty result image + result = np.zeros_like(image) + + # Apply the Gaussian filter + for i in range(padding, paddedImage.shape[0] - padding): + for j in range(padding, paddedImage.shape[1] - padding): + # Extract the region of interest (ROI) from the padded image + roi = paddedImage[i - padding:i + padding + 1, j - padding:j + padding + 1] + + # Convolve the ROI with the Gaussian kernel + filteredValue = np.sum(roi * kernel) + + # Assign the filtered value to the corresponding pixel in the result image + result[i - padding, j - padding] = filteredValue + + # If keepShape is False, remove the padding and resize the result image to match the input image shape + if not keepShape: + result = result[padding:result.shape[0] - padding, padding:result.shape[1] - padding] + + return result + +print(f"Original image: \n {image}") +print(f"After Gaussian filtering: \n {filteredImage}") +``` + +``` +Original Image: + [[ 1. 2. 3. 4. 5.] + [ 6. 7. 8. 9. 10.] + [11. 12. 13. 14. 15.] + [16. 17. 18. 19. 20.] + [21. 22. 23. 24. 25.]] + +Filtered Image: + [[ 1.7207065 2.8222058 3.5481372 4.274069 3.430702 ] + [ 4.629657 7. 8. 9. 6.985245 ] + [ 8.259314 12. 13. 14. 10.6149025] + [11.88897 17. 18. 19. 14.244559 ] + [10.270683 14.600147 15.326078 16.05201 11.9806795]] +``` + +#### 3.4. Bộ lọc Bilateral (Bilateral Filter) +Bộ lọc này được cải tiến từ bộ lọc Gauss nhằm giữ lại được nhiều chi tiết hơn. Nếu như bộ lọc Gauss chỉ quan tâm tới khoảng cách của các pixels xung quanh thì bilateral filter còn quan tâm tới giá trị của các pixels xung quanh. + +![Image](https://ailearningcentre.wordpress.com/wp-content/uploads/2017/05/bilateral_filter.jpg?w=800) + +Gauss filter và average filter làm mờ ảnh với mục đích là khử nhiễu và làm mịn ảnh, tuy nhiên chúng làm mất độ chi tiết như góc, cạnh trong tấm ảnh. Với mục tiêu là làm mịn ảnh, và khử nhiễu nhưng có thể giữ lại được nhiều chi tiết hơn so với hai bộ lọc tiền nhiệm, bilateral filter được ra đời. Bộ lọc này tích hợp spatial weight và intensity weight (một vài tài liệu gọi là range weight) để quyết định giá trị mới của pixel. + +Dưới đây là công thức tổng quát của Bilateral filter: + +$$I^{filtered}(x) = \frac{1}{W_p}\sum_{x_i \in \omega}I(x_i)W_p$$ + +Với + +$$W_p = \sum_{x_i \in \omega}f_r(||I(x_i) - I(x)||)g_s(||x_i - x||)$$ + + +$$ f_r(||I(x_i) - I(x)||) \text{ : Intensity weight}$$ +$$g_s(||x_i - x||) \text{ : Spatial weight}$$ +là 2 functions các bạn có thể chọn tùy ý, có thể là L2, có thể là L1, nhưng đa số đã thử nghiệm Gaussian function và chứng tỏ mức độ hiệu quả của việc dùng hàm này. + +Dưới đây là 2 công thức dùng Gaussian function cho intensity weight và spatial weight: + +$$f_r(||I(i, j) - I(k, l)||) = exp(-\frac{(||I(i, j) - I(k, l)||)^2}{2\sigma_r^2})$$ + +$$g_s(i, j, k, l) = exp(-\frac{(i-k)^2 + (j-l)^2}{2\sigma_s^2})$$ + +Notations: + +$$I^{filtered}: \text{Ảnh sau khi qua bộ lọc}$$ + +$$I: \text{Ảnh gốc}$$ + +$$x (i, j): \text{vị trí của pixel được filtered}$$ + +$$x_i (k, l): \text{vị trí của pixel xung quanh}$$ + +$$\omega : \text{vùng có trọng tâm tại x, và } x_i \text{ là một điểm trong vùng này}$$ + + +Tổng hợp lại, ta có hai công thức ngắn gọn sau để implement + +$$w(i, j, k, l) = exp(-\frac{(i-k)^2 + (j-l)^2}{2\sigma_d^2} - \frac{||I(i, j) - I(k, l)||^2}{2\sigma_r^2})$$ + +$$I^{filtered}(i, j) = \frac{\sum_{k, l}w(i, j, k, l)I(k, l)}{\sum_{k, l}w(i, j, k, l)}$$ + + + + + + +**Code** + +Dưới đây là đoạn code để implement bilateral filter cho 1 điểm ảnh + +**Step 1**: Tạo một tấm ảnh ngẫu nhiên để demo +```python +import numpy as np + +image = np.array(range(1, 26)).astype("float32") +print(f"Original Image: \n {image}") +``` + +``` +Original Image: +array([[ 1., 2., 3., 4., 5.], + [ 6., 7., 8., 9., 10.], + [11., 12., 13., 14., 15.], + [16., 17., 18., 19., 20.], + [21., 22., 23., 24., 25.]], dtype=float32) +``` + +**Step 2**: Bilateral filter cho 1 điểm ảnh + +```python + +x = 1 +y = 1 +sigmaD = 1 +sigmaR = 1 +d = 1 + +paddedImage = cv2.copyMakeBorder(image, d, d, d, d, cv2.BORDER_REFLECT_101) +gaussian = lambda val, sigma: (np.exp(-0.5 * val / sigma**2)) + +pts = [(y, x)] +for i in range(-d, d+1): + if i != 0: + pts.append((y+i, x)) + pts.append((y, x+i)) +deno = 0. +nume = 0. +for pt in pts: + weight = gaussian((pt[0] - y)**2 + (pt[1] - x)**2, sigmaD)*gaussian((paddedImage[pt] - paddedImage[y, x])**2, sigmaR) + deno += weight + nume += weight*paddedImage[pt] +print(f"Pixel value at {x, y} after being filtered: {nume/deno}") +``` +``` +Pixel value at (1, 1) after being filtered: 1.4238950333694957 +``` + +Và đó là cách tính giá trị mới của điểm ảnh sau khi được áp bộ lọc bilateral. Dưới đây sẽ là đoạn code hoàn chỉnh để dùng cho một tấm ảnh 2D (3D tương tự nhưng sẽ cần thêm 1 vòng loop cho channels) + +```python + +# Good to deploy +def bilateralFilter(image: np.ndarray, + d: int = 1, + sigmaD: float = 1., + sigmaR: float = 1.): + ''' + Implementation of bilateral filter by Mikyx-1, Le Hoang Viet + Args: + image (np.ndarray): 2D|Gray Image + d (int): Diameter + sigmaD (float): Sigma for distance + sigmaR (float): Sigma for range + + Returns: + output (np.ndarray): Filtered image + ''' + + gaussian = lambda val, sigma: (np.exp(-0.5 * val / sigma**2)) + + paddedImage = cv2.copyMakeBorder(image, d, d, d, d, cv2.BORDER_REFLECT_101) + imageHeight, imageWidth = paddedImage.shape + res = np.zeros((imageHeight, imageWidth)).astype(paddedImage.dtype) + for y in range(d, imageHeight-d): + for x in range(d, imageWidth-d): + pts = [(y, x)] + for i in range(-d, d+1): + if i != 0: + pts.append((y+i, x)) + pts.append((y, x+i)) + deno = 0. + nume = 0. + for pt in pts: + weight = gaussian((pt[0] - y)**2 + (pt[1] - x)**2, sigmaD)*gaussian((paddedImage[pt] - paddedImage[y, x])**2, sigmaR) + deno += weight + nume += weight*paddedImage[pt] + res[y, x] = nume/deno + return res + +print(np.allclose(cv2.bilateralFilter(image, 1, 20, 20, cv2.BORDER_CONSTANT), bilateralFilter(image, 1, 20, 20))) +``` + +``` +True +``` + +### 4. Kết luận +Mình đã hướng dẫn các bạn các bộ lọc cơ bản và thông dụng nhất trong lĩnh vực xử lý ảnh. Ngoài ra, chúng ta cũng đã code from scratch để hiểu rõ chính xác cách hoạt động của chúng. Tuy nhiên, các bộ lọc này đã khá cũ và hiện tại ở thời điểm này đã có nhiều bộ lọc tốt hơn về cả chất lượng và tốc độ, và trong bài post tiếp theo mình sẽ giới thiệu và cũng sẽ code from scratch những bộ lọc này nhé. + +Đây là một bài viết dài, cảm ơn các bạn đã đọc tới đây nha. Hãy feedback mình qua email hoặc github nếu bạn thấy có bất cứ lỗi hay chỗ nào chưa hợp lý nhé. + +### References + +1. [Bilateral Filter - Wikipedia](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html) + + + diff --git a/_posts/2024-04-27-image_filters_p2.md b/_posts/2024-04-27-image_filters_p2.md new file mode 100644 index 00000000000..679589cdb26 --- /dev/null +++ b/_posts/2024-04-27-image_filters_p2.md @@ -0,0 +1,282 @@ +--- +title: "Giải thích và implement các bộ lọc trong xử lý ảnh - Phần 2" +mathjax: true +layout: post +--- + +Xin chào các bạn, + +Nối tiếp series phần 1 của bộ lọc, bài post này sẽ giới thiệu thêm một vài bộ lọc nâng cao hơn của phần 1 với trọng tâm chính là tìm cạnh trong ảnh (edge detection). + +![Image](https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/%C3%84%C3%A4retuvastuse_n%C3%A4ide.png/500px-%C3%84%C3%A4retuvastuse_n%C3%A4ide.png) + + +### 1. Giới thiệu về edge detection + +Phát hiện cạnh là giải thuật để trích xuất ra tất cả các cạnh trong một bức ảnh. Việc trích xuất ra được edge này có rất nhiều công dụng hữu ích cho việc phát hiện đường thẳng, trích xuất features, phát hiện blob, ... Và những ứng dụng của nó vào khâu tiền xử lý của những thuật toán này sẽ được mình cover vào các bài khác (các bạn nhớ đón đọc nhé). + +### 2. Cách hoạt động +Cách để tìm ra cạnh trong một bức ảnh đơn giản là tìm những đoạn mà có sự thay đổi lớn giữa các giá trị pixels hoặc giá trị gradient tại điểm đó lớn. Mình lấy một ví dụ minh họa như sau: + +``` +Image = [0. 1. 1. 0.] + [0. 1. 1. 0.] + [0. 1. 1. 0.] + [0. 1. 1. 0.] +``` + +Giả sử ở hình trên, ta thấy được rằng có 1 cạnh nằm giữa tấm ảnh (2 cột 1.). Và ta muốn lọc ra được cạnh này. + +Nếu quan sát có thể nhận thấy rằng, ở những cạnh như vậy giá trị gradient sẽ có giá trị lớn. Và đó cũng là nguyên lý mà nhiều thuật toán áp dụng để tìm ra cạnh trong một bức ảnh. Ta sẽ dùng gradient để xác định xem liệu đó có phải là cạnh hay không. Công thức để tính gradient với các pixels như sau: + +$$\frac{\delta f(x, y)}{\delta x \delta y} = \frac{\delta f(x, y)}{\delta x} + \frac{\delta f(x, y)}{\delta y}$$ + +Với + +$$\frac{\delta f(x, y)}{\delta x} = f(x+1, y) - f(x, y)$$ + +$$\frac{\delta f(x, y)}{\delta y} = f(x, y+1) - f(x, y)$$ + + +### 3. Các loại bộ lọc +Trong phần này, mình sẽ giới thiệu các bộ lọc cạnh cũng như demo code để minh họa rõ hơn về cách hoạt động của các bộ lọc này. + + +### 3.1. Prewitt filter +Bộ lọc Prewitt filter được phát triển bởi Judith M. S. Prewitt và bộ lọc này hoạt động y như phần lý thuyết mình đã mô tả ở trên. Tuy nhiên, Prewitt đã có chỉnh sửa một chút là dùng đạo hàm giữa 2 pixel cách nhau 1 pixel thay vì lấy 2 pixels liên tiếp để tính đạo hàm. +``` +kernelX = [-1 0 1] + [-1 0 1] + [-1 0 1] + +kernelY = [-1 -1 -1] + [0 0 0] + [1 1 1] +``` + + + +**Code** +```python +### Implement Prewitt Filter + +prewittGx = np.array([[-1, 0, 1], + [-2, 0, 2], + [-1, 0, 1]]) + +prewittGy = np.array([[-1, -2, -1], + [0, 0, 0], + [1, 2, 1]]) + +prewittX = cv2.filter2D(image, -1, prewittGx) +prewittY = cv2.filter2D(image, -1, prewittGy) +prewitt = prewittX + prewittY + + +fig, ax = plt.subplots(1, 4, figsize = (15, 15)) +ax[0].imshow(image) +ax[0].set_title("Prewitt in X direction") + +ax[1].imshow(prewittX) +ax[1].set_title("Prewitt in X direction") + +ax[2].imshow(prewittY) +ax[2].set_title("Prewitt in Y direction") + +ax[3].imshow(prewitt) +ax[3].set_title("Prewitt Filter") + +``` + +![Alt text](../_data/images/image_filters_p2/image-4.png) + + +### 3.2. Roberts filter +Roberts filter là bộ lọc được tìm ra bởi Lawrence Roberts vào năm 1963. Đây có thể được coi như là một trong những bộ lọc được ra đời sớm nhất trong tất cả các bộ lọc edge detection. + +Vì nó được ra mắt sớm hơn các bộ lọc cạnh khác nên có có cấu tạo khá đơn giản. Bộ lọc bao gồm 2 ma trận 2x2 như sau: + +``` +kernelX = [-1 0] + [0 1] + +kernelY = [0 1] + [-1 0] +``` + +Tuy được gán là $$G_x \text{và } G_y$$ nhưng hai ma trận này không tính gradient theo hai phương đó mà tính theo 2 phương chéo nhau (mình đặt vậy để dễ phân biệt). + +**Code** +```python +### Implement Roberts Filter +robertsGx = np.array([[1, 0], + [0, -1]]) + +robertsGy = np.array([[0, 1], + [-1, 0]]) + +robertsX = cv2.filter2D(image, -1, robertsGx) +robertsY = cv2.filter2D(image, -1, robertsGy) + +roberts = robertsX + robertsY + +fig, ax = plt.subplots(1, 4, figsize = (15, 15)) +ax[0].imshow(image) +ax[0].set_title("Raw Image") + +ax[1].imshow(robertsX) +ax[1].set_title("Roberts in X direction") + +ax[2].imshow(robertsY) +ax[2].set_title("Roberts in Y direction") + +ax[3].imshow(roberts) +ax[3].set_title("Roberts filter") +``` + +![Alt text](../_data/images/image_filters_p2/image-2.png) + + +### 3.3. Sobel filter + +Tên đầy đủ của bộ lọc này là Sobel-Feldman được đặt tên theo 2 người cùng nghiên cứu và phát triển nó tại AI lab của đại học Standford. Bộ lọc Sobel về cơ bản giống y chang bộ lọc Prewitt, chỉ khác ở chỗ phần pixel trọng tâm của kernel sẽ được đánh trọng số là 2 thay vì 1 như Prewitt filter. + +Sobel filter hoạt động theo kiểu tích chập. Nó gồm hai ma trận $$G_x, \text{ và } G_y$$ và nó có dạng như sau: + +``` +kernelX = [-1 0 1] + [-2 0 2] + [-1 0 1] + +kernelY = [-1 -2 -1] + [0 0 0] + [1 2 1] +``` + +**Code** + +```python +### Implement Sobel Filter + +sobelGx = np.array([[-1, 0, 1], + [-2, 0, 2], + [-1, 0, 1]]) + +sobelGy = np.array([[-1, -2, -1], + [0, 0, 0], + [1, 2, 1]]) + +sobelX = cv2.filter2D(image, -1, sobelGx) +sobelY = cv2.filter2D(image, -1, sobelGy) +sobel = sobelX + sobelY + +fig, ax = plt.subplots(1, 4, figsize = (15, 15)) +ax[0].imshow(image) +ax[0].set_title("Image") + +ax[1].imshow(sobelX) +ax[1].set_title("Sobel in X direction") + +ax[2].imshow(sobelY) +ax[2].set_title("Sobel in Y direction") + +ax[3].imshow(sobel) +ax[3].set_title("Sobel") +``` + +![Alt text](../_data/images/image_filters_p2/image-3.png) + + + +### 3.4. Laplacian filter +Laplacian filter áp dụng thêm đạo hàm bậc 2 để tìm ra cạnh. Việc sử dụng đạo hàm bậc 2 này sẽ giúp phát hiện ra nhiều cạnh hơn so với các phương pháp trên. Tuy nhiên, nó cũng sẽ dễ bị ảnh hưởng bởi noise nhiều hơn. + +**Công thức toán** + +$$\Delta ^2f = \frac{\delta ^2 f}{\delta x^2} + \frac{\delta ^2 f}{\delta y^2}$$ + +Theo phương x, + +$$\frac{\delta ^2 f}{\delta x^2} = f(x+1, y) + f(x-1, y) - 2f(x, y)$$ + +Theo phương y, + +$$=> \Delta ^2 f = f(x+1, y) + f(x-1, y) _ f(x, y-1) + f(x, y+1) + 4f(x, y)$$ + + +Nếu thích, chúng ta có thể tách chúng ra thành hai bộ lọc như phương pháp Sobel hoặc gom lại thành một. Nếu tách ra, nó sẽ có dạng như sau: + +``` +kernelX = [1 -2 1] + [1 -2 1] + [1 -2 1] + +kernelY = [1 1 1] + [-2 -2 -2] + [1 1 1] +``` + +Và nếu gom lại, chúng sẽ có dạng như sau: + +``` +kernel = [0 1 0] + [1 -4 1] + [0 1 0] +``` + +**Code** +```python +### Implement Laplacian Filter + +laplacianKernel = np.array([[0, 1, 0], + [1, -4, 1], + [0, 1, 0]]) + +laplacianKernelX = np.array([[1, -2, 1], + [1, -2, 1], + [1, -2, 1]]) + +laplacianKernelY = np.array([[1, 1, 1], + [-2, -2, -2], + [1, 1, 1]]) + +laplacianFiltered = cv2.filter2D(image, -1, laplacianKernel) + +laplacianFilteredX = cv2.filter2D(image, -1, laplacianKernelX) +laplacianFilteredY = cv2.filter2D(image, -1, laplacianKernelY) +laplacianFiltered_ = laplacianFilteredX + laplacianFilteredY + +fig, ax = plt.subplots(1, 5, figsize = (25, 25)) +ax[0].imshow(image) +ax[0].set_title("Image") + +ax[1].imshow(laplacianFiltered) +ax[1].set_title("Laplacian Filter") + +ax[2].imshow(laplacianFilteredX) +ax[2].set_title("Laplacian Filter X") + +ax[3].imshow(laplacianFilteredY) +ax[3].set_title("Laplacian Filter Y") + +ax[4].imshow(laplacianFiltered_) +ax[4].set_title("Laplacian with 2 kernels") +``` + +![Alt text](../_data/images/image_filters_p2/image-5.png) + + +### 3. Lời kết +Trong 4 bộ lọc trên, bộ lọc Laplacian tìm ra được nhiều cạnh nhất tuy nhiên nó cũng bị noise nhiều nhất. Việc dùng bộ lọc nào sẽ tùy thuộc vào nhu cầu của các bạn. Nếu hình có ít nhiễu và muốn lọc ra được nhiều cạnh nhất thì bộ lọc Laplacian là tối ưu nhất, còn 3 bộ lọc kia có sự khác biệt không đáng kể do chúng đều dùng đạo hàm bậc 1. + + +### References +1\. [Roberts Cross - Wikipedia](https://en.wikipedia.org/wiki/Roberts_cross) + +2\. [Prewitt Operator - Wikipedia](https://en.wikipedia.org/wiki/Prewitt_operator) + +3\. [Sobel Operator - Wikipedia](https://en.wikipedia.org/wiki/Sobel_operator) + +4\. [Discrete Laplace Operator - Wikipedia](https://en.wikipedia.org/wiki/Discrete_Laplace_operator) + + diff --git a/_posts/2024-04-28-canny_edge.md b/_posts/2024-04-28-canny_edge.md new file mode 100644 index 00000000000..c910199f641 --- /dev/null +++ b/_posts/2024-04-28-canny_edge.md @@ -0,0 +1,198 @@ +--- +title: "Giải thích và code giải thuật Canny Edge Detection" +mathjax: true +layout: post +categories: media +--- + +Xin chào các bạn, + +Trong bài post về các [bộ lọc phần 2][này], mình đã giới thiệu về các bộ lọc cạnh và cũng như cách hoạt động của chúng. Ở phần này, mình sẽ giới thiệu một giải thuật cho kết quả vượt trội hơn các phương pháp trước có tên là Canny edge detection. Giải thuật này được phát triển bởi John F. Canny vào năm 1986. Nó bao gồm nhiều bước hậu xử lý để có thể cải thiện kết quả so với các phương pháp dựa vào đạo hàm. + +### 1. Điểm yếu của việc dùng đạo hàm để tìm cạnh +Các bộ lọc như Sobel, Prewitt, Laplacian dựa vào **mỗi** giá trị gradient của pixel để tìm ra cạnh. Tuy nhiên, trong ảnh thu được từ camera sẽ có rất nhiều nguồn nhiễu từ cả bên ngoài và bên trong, và điều này khiến cho các phương pháp này kém ổn đỉnh. + +### 2. Giải thuật Canny +Giải thuật Canny về cơ bản bao gồm 5 bước. Chúng ta hãy cùng xem giải thuật này hoạt động như thế nào và code nó from scratch nhé. + +**Bước 1**: Giảm nhiễu +Để có thể giảm noise và tìm ra các cạnh đáng tin cậy, một bộ lọc thông thấp với mục đích làm mịn ảnh sẽ được áp dụng. + +Bộ lọc có thể tùy ý lựa chọn, các bạn có thể chọn bộ lọc trung bình, trung vị, Gauss, bilateral, ... miễn là nó làm mịn ảnh. Ở đây mình sẽ dùng Gaussian filter để demo. + +```python +import cv2 +import matplotlib.pyplot as plt +import numpy as np + +# Read image and turn into gray +# Can be replaced with any image + +# Step 1: Gaussian Blur +image = cv2.imread("./lena.png", 0) +blurred = cv2.GaussianBlur(image) +``` +![Alt text](image.png) + +**Bước 2**: Tính gradient và orientation + +Ở bước này, ta sẽ dùng các bộ lọc như ở bài [này][này] để có tìm ra được các cạnh trong tấm ảnh. Các bạn có thể dùng bất kì bộ lọc gradient-based nào để tìm cạnh cũng được nhé. + +Ở bước này, ta cần tính cường độ và hướng gradient tăng nhanh nhất (hướng của cạnh). Và điều này được tính theo công thức sau: + +$$G = \sqrt{G_x^2 + G_y ^2}$$ + +$$\theta = arctan(\frac{G_y}{G_x})$$ + +```python + +# Step 2: Find all edges +def sobelFilter(image): + image_ = image.copy()/255. + + Gx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype = np.float32) + Gy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype = np.float32) + + Ix = cv2.filter2D(image_, ddepth=-1, kernel = Gx) + Iy = cv2.filter2D(image_, ddepth=-1, kernel=Gy) + + G = np.hypot(Ix, Iy) + G = G/G.max() + theta = np.arctan2(Iy, Ix) + return G, theta/np.pi * 180 + +G, theta = sobelFilter(blurred) +``` + + +![Alt text](image-1.png) + + +**Bước 3**: Non-Max Suppression + +Ở bước 2, chúng ta đã lọc ra được gần như tất cả các cạnh trong tấm ảnh, nhưng không phải tất cả chúng đều là cạnh. Vì thế, cần có một bước tắt (suppress) các điểm ảnh mà khả năng cao không phải là cạnh. + +Theta cho chúng ta biết hướng tăng nhanh nhất của gradient (tức là hướng của cạnh phát hiện được), vì vậy nếu tại hướng đó, giá trị biên độ đạo hàm là lớn nhất thì sẽ được giữ lại và nếu không phải thì ta sẽ set về 0. + +Với domain là ảnh, sẽ chỉ có 4 hướng cho một pixel: Hướng ngang, hướng chéo từ trái sang phải, hướng dọc từ trên xuống, hướng chéo từ phải sang trái. Và vì chỉ có 4 hướng nên ta sẽ chia thành các khoảng. Mình sẽ chia đều thành các khoảng như sau: + +$$0 <= \theta <= 22.5 \text{ or } 157.5 < \theta <= 180 : \text{phương ngang}$$ + +$$22.5 < \theta <= 67.5: \text{phương chéo từ trái sang phải}$$ + +$$67.5 < \theta <= 112.5: \text{phương dọc từ trên xuống}$$ + +$$112.5 < \theta <= 157.5: \text{phương chéo từ phải sang trái}$$ + + +![Image](https://archive.ph/rUlFC/98bbb6b6f5ed2abafece9c7cd61d8f31ef96ec33.png) + + +**Code** + +```python +def nonMaxSuppression(image, theta): + if image.ndim > 2 or image.ndim < 2: + raise "Image must be gray, 2D" + + M, N = image.shape + Z = np.zeros_like(image) + theta[theta<0] += 180 + + for i in range(1, M-1): + for j in range(1, N-1): + if 0 <= theta[i, j] <= 22.5 or 157.5 <= theta[i, j] <= 180: + firstValue = image[i, j-1] + secondValue = image[i, j+1] + elif 22.5 < theta[i, j] <= 67.5: + firstValue = image[i-1, j-1] + secondValue = image[i+1, j+1] + elif 67.5 < theta[i, j] <= 112.5: + firstValue = image[i-1, j] + secondValue = image[i+1, j] + else: + firstValue = image[i-1, j+1] + secondValue = image[i+1, j-1] + + if image[i, j] >= firstValue and image[i, j] >= secondValue: + Z[i, j] = image[i, j] + else: + Z[i, j] = 0 + return Z + +suppressed = nonMaxSuppression(G, theta) +``` +![Alt text](image-2.png) + +**Bước 4**: Double Threshold + +Ở bước 4 này, double threshold sẽ được áp dụng để phân các giá trị pixel ra thành 3 loại: edge, weak edge, not edge. + +![Image](https://docs.opencv.org/4.x/hysteresis.jpg) + +Như ở hình minh họa, nếu giá trị pixel lớn hơn maxVal ta sẽ gán là edge, dưới minVal là not edge, và ở giữa maxVal và minVal là weak edge. + + +**Code** + +```python +def threshold(image, lowThreshold=0.03, highThreshold = 0.05): + image = np.where(image>=highThreshold, 1., image) + image = np.where(image<=lowThreshold, 0., image) + return image + +thresholded = threshold(suppressed, lowThreshold=0.02, highThreshold=0.075) +``` + +![Alt text](image-3.png) + +**Bước 5**: Edge Tracking by hysteresis + +Ở bước này, với các weak edge nếu nằm gần strong edge sẽ được đổi thành edge và nếu xung quanh nó không có strong edge thì nó sẽ thành not edge, vì thường các cạnh sẽ được nối liền với nhau chứ không tách rời. + +**Code** + +```python +def hysteresis(image): + image_ = image.copy() + image_ = cv2.copyMakeBorder(image_, 1, 1, 1, 1, cv2.BORDER_CONSTANT, 0) + for y in range(1, image_.shape[0]-1): + for x in range(1, image_.shape[1]-1): + if image_[y, x] > 0. and image_[y, x] < 1.: + if image_[y-1, x-1] == 1. or image_[y-1, x] == 1. or image_[y-1, x+1] == 1. or \ + image_[y, x-1] == 1. or image_[y, x+1] == 1. or image_[y+1, x-1] == 1. or \ + image_[y+1, x] == 1. or image_[y+1, x+1] == 1.: + image_[y, x] = 1. + else: + image_[y, x] = 0. + return image_[1:-1, 1:-1] +image_ = hysteresis(thresholded) +``` + +![Alt text](image-4.png) + +Sau bước 5 này, ảnh kết quả sẽ có 2 giá trị với 0 là not edge và 1 là edge (các đường cam là do lỗi hiển thị). Mình để 2 ảnh nếu chỉ dùng Sobel và dùng Canny để minh họa. Và như các bạn thấy, các cạnh trong phương pháp Canny mỏng hơn và ít có nhiễu hơn so với bộ lọc Sobel. + +### 3. Lời kết + +Trong phần này, chúng ta đã đi sâu vào việc giải thích và cùng xây dựng thuật toán Canny from scratch. Không chỉ là công cụ để tìm kiếm cạnh trong hình ảnh, thuật toán này còn có thể được sử dụng như một bước tiền xử lý quan trọng cho nhiều ứng dụng khác nhau trong xử lý ảnh, bao gồm việc tìm contours, phát hiện đường thẳng, ... + +Việc hiểu rõ về cách hoạt động của thuật toán Canny không chỉ giúp chúng ta áp dụng nó hiệu quả vào các task khác của mình mà còn là cơ hội để nâng cao kiến thức về xử lý ảnh. Hy vọng rằng thông qua bài viết này, bạn đã có được cái nhìn tổng quan và sự hiểu biết sâu sắc hơn về thuật toán Canny và vai trò quan trọng của nó trong lĩnh vực xử lý ảnh. + + +### References +1\. [Canny Edge Detection - OpenCV](https://docs.opencv.org/4.x/da/d22/tutorial_py_canny.html) +2\. [Canny Edge Detection Step by Step in Python - Computer Vision - Towards Data Science](https://towardsdatascience.com/canny-edge-detection-step-by-step-in-python-computer-vision-b49c3a2d8123) + + +[này]: https://mikyx-1.github.io/image_filters_p2/ + + + + + + + + + + diff --git a/_posts/2024-05-09-fourier_transform.md b/_posts/2024-05-09-fourier_transform.md new file mode 100644 index 00000000000..305de2cca49 --- /dev/null +++ b/_posts/2024-05-09-fourier_transform.md @@ -0,0 +1,208 @@ +--- +title: "Giải thích Fourier Transform" +mathjax: true +layout: post +categories: media +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ giới thiệu về chuỗi Fourier và biến đổi Fourier. Đây là một giải thuật có rất nhiều công dụng trong Vật lý, toán học, kỹ thuật viễn thông, kỹ thuật tự động, ... Ngoài ra, biến đổi Fouier có thể có rất rất nhiều ứng dụng ngoài đời thật. Vì nó rất quan trọng và có rất nhiều thứ ta có thể áp dụng nó nên mình sẽ đi sâu vào việc chứng minh nó để có thể giúp các bạn hiểu được giải thuật này hoạt động như thế nào. + +![Image](https://media.licdn.com/dms/image/C5112AQG_liNXZZMl1A/article-cover_image-shrink_423_752/0/1577030802664?e=1720656000&v=beta&t=C9L_PmbXvIobSYPQEPW_WSH-hEsAZQx2Lez3-qciGwE) + + + + +### 1. Đặt vấn đề +Trước khi đi vào phần chứng minh, mình muốn dành một phần nhỏ để nói về ứng dụng của giải thuật Fourier. + + +![Image](https://www.ksmotor.tw/uploads/editor/files/quality-2-1.jpg) + + + +Giả sử trong khâu kiểm định chất lượng motor trước khi xuất xưởng, chúng ta phải test thử motor chạy tốt hay không tốt để giao tới khách hàng. Việc này có thể test bằng việc cho motor chạy khoảng vài chục ngàn vòng và nghe tiếng phát ra của nó. Nếu nó chạy đều và ổn định thì nó là motor tốt còn trong lúc test nó rít lên hay phát ra tiếng kêu to hoặc nhỏ hơn bình thường, ta cần loại những sản phầm này. Những tiếng rít của motor có tần số rất cao và ta muốn phát hiện những tiếng rít này một cách tự động. Tuy nhiên, tín hiệu ta thu về có đồ thị như ở hình dưới, chỉ có đơn điệu đồ thị biên độ và thời gian. + +![Image](https://www.researchgate.net/profile/Rahul-Chaurasiya/publication/268391294/figure/fig1/AS:614119958917140@1523429016448/Voice-signal-for-the-word-one-The-1-second-duration-of-the-time-axis-is-divided-into.png) + + +Chuỗi Fourier sẽ giúp chúng ta tách sóng này ra thành các thành phần sin và cos với nhiều tần số khác nhau và biến đổi Fourier sẽ cho chúng ta biết đích xác có nững dải tần số nào trong tín hiệu này. Nhờ những tín hiệu này, ta có thể dễ dàng phân loại được chất lượng motor một cách tự động. + +### 1. Chuỗi Fourier (Fourier Series) +Chuỗi Fourier là tổng hợp của các tín hiệu sin và cos để có thể tạo nên tín hiệu gốc. Điều kiện để có thể phân tách được thành chuối Fourier là nó phải có tính tuần hoàn. + +$$f(x + T) = f(x)$$ + +Và nó có công thức như sau: + +$$f(x) = \sum_{n=0}^{\infty}a_ncos(\frac{n\pi x}{L}) + \sum_{n=0}^{\infty}b_nsin(\frac{n\pi x}{L}) \text{ } (1)$$ + +Vì $$cos(0) = 1$$ và $$sin(0) = 0$$ nên ta có thể rút $$(1)$$ thành như sau: + +$$<=> f(x) = a_0 + \sum_{n=1}^{\infty}a_ncos(\frac{n\pi x}{L}) + \sum_{n=1}^{\infty}b_nsin(\frac{n\pi x}{L}) \text{ } (2)$$ + +Đặt $$\theta_n = \frac{n\pi}{L}$$ và áp vào $$(2)$$ ta được: + +$$f(x) = a_0 + \sum_{n=1}^{\infty}a_ncos(\theta_n x) + \sum_{n=1}^{\infty}b_nsin(\theta_n x) \text{ } (3)$$ + + + +Theo Euler, ta có: + +$$cos(\theta) = \frac{e^{j \theta} + e^{-j \theta}}{2}$$ + +$$sin(\theta) = \frac{e^{j \theta} - e^{-j \theta}}{2j}$$ + +Áp 2 công thức trên vào (3) ta được: + +$$<=>f(x) = a_0 + \sum_{n = 1}^{\infty}\frac{a_n}{2} (e^{j \theta_n x} + e^{-j \theta_n x}) + \sum_{n = 1}^{\infty}\frac{b_n}{2j} (e^{j \theta_n x} - e^{-j \theta_n x}) \text{ } (4)$$ + +Khai triển từ (4), ta được: + +$$<=>f(x) = a_0 + \sum_{n = 1}^{\infty}(\frac{a_n}{2} + \frac{b_n}{2j})e^{j \theta_n x} + \sum_{n = 1}^{\infty}(\frac{a_n}{2} - \frac{b_n}{2j})e^{-j \theta_n x} \text{ } (5)$$ + +Triển khai tiếp từ (5), ta được: + +$$<=> f(x) = a_0 + \sum_{n = 1}^{\infty}(\frac{a_n - j b_n}{2})e^{j \theta_n x} + \sum_{n = 1}^{\infty}(\frac{a_n + j b_n}{2})e^{-j \theta_n x} \text{ } (6)$$ + +Ta biến đổi tiếp từ (6) như sau: + +Đặt + +$$C_{n-} = \frac{a_n - j b_n}{2}$$ + +$$C_{n+} = \frac{a_n + j b_n}{2}$$ + +$$C_0 = a_0$$ + +$$f(x) = C_0 + \sum_{n = -1}^{-\infty}C_{n-}e^{-j \theta_n x} + \sum_{n = 1}^{\infty}C_{n+}e^{-j \theta_n x} \text{ } (6)$$ + +Và ta có được công thức tổng quát của chuỗi Fourier: + +$$<=> f(x) = \sum_{n = - \infty}^{\infty} C_n e^{-j \theta_n x} \text{ } (7)$$ + +Sau khi có được công thức tổng quát, ta cần tìm ra các hệ số $$a_n, b_n, c_n$$. Và ý tưởng tìm ra các hệ số này là tính trực giao (Orthogonality). + +Ta có mệnh đề trực giao đối với một cặp hàm số điều hòa như sau: + +$$\int_{-\pi}^{\pi} cos(nx)cos(kx)dx = 0 \text{ } (n \neq k)$$ + + +$$\int_{-\pi}^{\pi} sin(nx)cos(kx)dx = 0$$ + + +**Note**: Mình sẽ chứng minh mệnh đề này ở sau cùng, bây giờ hãy tập trung vào chuỗi Fourier trước. + +Bây giờ, ta hãy nhân cả 2 vế của (1) với cos(x), ta được: + +$$\int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx = \int_{-\pi}^{\pi}(\sum_{n=0}^{\infty}a_ncos(\theta_n x) + \sum_{n=0}^{\infty}b_nsin(\theta_n x))cos(\theta x) dx \text{ } (8)$$ + +Khai triển tiếp, ta được: + +$$<=>\int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx = \int_{-\pi}^{\pi}(\sum_{n=0}^{\infty}a_ncos(\theta_n x) + \sum_{n=0}^{\infty}b_nsin(\theta_n x))cos(\theta_n x) dx \text{ } (8)$$ + + +$$<=>\int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx = \int_{-\pi}^{\pi}(\sum_{n=0}^{\infty}a_ncos(\theta_n x) + \sum_{n=0}^{\infty}b_nsin(\theta_n x))cos(\theta_n x) dx \text{ } (9)$$ + +$$<=>\int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx = \int_{-\pi}^{\pi} a_ncos(\theta_k x)cos(\theta_n x) dx \text{ } (10)$$ + +$$<=>\int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx = a_n \pi \text{ } (11)$$ + +$$ => a_n = \frac{1}{\pi} \int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx \text{ } (12)$$ + +$$ => a_0 = \frac{1}{2\pi} \int_{-\pi}^{\pi} f(x)dx \text{ = average of f(x) } (13)$$ + + +Tương tự, ta có $$b_n$$ được tính như sau: + +$$ => b_n = \frac{1}{\pi} \int_{-\pi}^{\pi} f(x)sin(\theta_n x)dx \text{ } (14)$$ + + +Và sau khi có giá trị $$a_n \text{ và } b_n$$, ta có thể phân tích bất kì dạng sóng nào thành một chuỗi sin và cos. + +Ví dụ tính toán chuỗi Fourier: + +Phân tích hàm dirac như hình dưới thành một chuỗi các sóng sin và cos + +![Image](https://wikiwandv2-19431.kxcdn.com/_next/image?url=https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Dirac_distribution_PDF.svg/langvi-1500px-Dirac_distribution_PDF.svg.png&w=1200&q=50) + + +Như ở hình trên, có thể thấy rằng hàm dirac là hàm chẵn do $$f(x) = f(-x)$$, vậy nên sẽ không tồn tại các các hàm $$sin$$. Vì vậy, hàm dirac sẽ chỉ bao gồm các dạng sóng $$cos$$. + +Dựa vào quan sát trên, ta khai triển như sau: + +$$f(x) = a_0 + \sum_{n=1}^{\infty}a_ncos(\theta_n x)$$ + +Như đã chứng minh ở trên, $$a_0$$ được tính bằng công thức: + +$$ a_0 = \frac{1}{2\pi} \int_{-\pi}^{\pi} f(x)dx = \frac{1}{2 \pi} $$ + +$$a_n = \frac{1}{\pi} \int_{-\pi}^{\pi} f(x)cos(\theta_n x)dx = \frac{1}{\pi}$$ + +Biết được $$a_n$$ và $$a_0$$, ta có thể xấp xỉ hàm dirac: + +$$\delta(x) = \frac{1}{2\pi} + \frac{1}{\pi}(cos(\frac{1}{2}x) + cos(x) + cos(\frac{3}{2}x) + cos(2x) + ...)$$ + +Ta được kết quả như sau nếu hiển thị nó matplotlib để trực quan hóa + +![Alt text](/_data/images/fourier/fourier_dirac_example.png) + + + + +Sau khi phân tích một tín hiệu thu được thành một chuỗi tín hiệu sin và cos, giờ ta muốn tìm xem trong chuỗi đó có những loại tần số nào? Fourier Transform sẽ cho chúng ta biết điều này. + + + + +### 2. Fourier Transform + +Fourier Transform chỉ đơn thuần là một phép biến đổi được triển khai từ chuỗi Fourier, nếu đã nắm chắc lý thuyết về chuỗi Fourier thì phần này sẽ rất dễ dàng với các bạn. + +Ta bắt đầu từ phương trình (7) ở trên, chuỗi Fourier được biểu diễn gọn gàng với công thức như sau: + + +$$f(x) = \sum_{n = - \infty}^{\infty} C_n e^{-j \theta_n x}$$ + +Diễn giải bằng lời thì mỗi dạng sóng bất kì sẽ được phân tách thành một chuỗi tín hiệu với tần số góc $$\theta_n$$. $$C_n$$ được tạo thành từ số phức (như chứng minh ở trên), vì vậy hệ số này sẽ bao gồm biên độ (Magnitude) và pha (phase) của mỗi tín hiệu $$\theta_n$$. Và chỉ cần tìm ra hệ số $$C_n$$ này thì ta có thể dễ dàng tìm ra trong dạng sóng này có những biên độ và pha nào. + +Theo như công thức thì ta có 3 trường hợp cho $$C_n$$: + +$$C_{n-} = \frac{a_n - j b_n}{2}$$ + +$$C_{n+} = \frac{a_n + j b_n}{2}$$ + +$$C_0 = a_0$$ + +Ở biến đổi Fourier, ta chỉ quan tâm đến biên độ và tần số, vì vậy việc $$C_{n-}$$ hay $$C_{n+}$$ sẽ không ảnh hưởng tới kết quả cuối cùng bởi vì kết quả đã được scale về cùng một tiêu chuẩn. + +Ở bài này, mình sẽ chọn $$C_{n-} = \frac{a_n - j b_n}{2} \text{ } (13)$$ + +Ta thay (12) và (14) vào (13), được: + +$$ <=> a_n - jb_n = \frac{1}{2 \pi} \int_{-\pi}^{\pi} f(x)(cos(\theta_nx) - jsin(\theta_nx))dx$$ + +Áp dụng định lý Euler, ta được dạng tổng quát và quen thuộc sau: + + +$$ <=> C_n = a_n - jb_n = \frac{1}{2 \pi} \int_{-L}^{L} f(x)e^{-j \theta_nx}dx$$ + + + +**Note**: Phần hệ số $$\frac{1}{2 \pi}$$ có thể được lược đi, và thông thường thì mình thấy người ta lược đi cho gọn. + +### 3. Phần kết + +Ở bài này, mình đã giới thiệu cho các bạn chuỗi Fourier, biến đổi Fourier và chứng minh chuỗi này bằng toán học. Thuật toán này làm được cực kì nhiều thứ hay ho, những ứng dụng của nó trong xử lý ảnh và deep learning sẽ được mình tổng hợp và trình bày trong các bài post tới. Giờ thì các bạn có thể phân tách bất kì hàm số nào thành chuỗi Fourier được rồi, chúc các bạn vui vẻ với nó nhé. + + +### References + +1\. [Deriving the Fourier Transform - BK Teach](https://www.youtube.com/watch?v=Q99ZPGnUBAQ&list=WL&index=71&t=533s) + +2\. [Fourier Series - MIT OpenCourseWare](https://www.youtube.com/watch?v=vA9dfINW4Rg&list=WL&index=76) + +3\. [Fourier Transform - Wikipedia](https://en.wikipedia.org/wiki/Fourier_transform) + +4\. [Difference between Fourier Series and Fourier Transform](https://www.tutorialspoint.com/difference-between-fourier-series-and-fourier-transform) \ No newline at end of file diff --git a/_posts/2024-05-10-naive_bayes.md b/_posts/2024-05-10-naive_bayes.md new file mode 100644 index 00000000000..1717479aba8 --- /dev/null +++ b/_posts/2024-05-10-naive_bayes.md @@ -0,0 +1,105 @@ +--- +title: "Naive Bayes" +mathjax: true +layout: post +categories: media +--- + +### 1. Giới thiệu + +Naive Bayes Classifier là một thuật toán phân loại dựa trên định lý Bayes, với giả định quan trọng là các đặc trưng của dữ liệu đều độc lập có điều kiện với nhau. Dù giả định này có thể không hoàn toàn đúng trong nhiều trường hợp, Naive Bayes vẫn hoạt động hiệu quả trong nhiều ứng dụng thực tế, đặc biệt là các bài toán phân loại văn bản như phát hiện spam, phân tích cảm xúc, và phân loại tài liệu. + +
+ +
+ +Naive Bayes được ưa chuộng nhờ vào sự đơn giản trong cài đặt, tốc độ nhanh chóng khi xử lý dữ liệu lớn, và khả năng mở rộng cho nhiều loại dữ liệu khác nhau. Trong bài viết này, chúng ta sẽ đi sâu vào phần toán học phía sau Naive Bayes, các biến thể phổ biến của nó, và phân tích ưu và nhược điểm của phương pháp này. + +### 2. Naive Bayes Classifier - Phần toán + +#### 2.1. Định lý Bayes + +Naive Bayes Classifier dựa trên định lý Bayes, công thức tổng quát như sau: + +$$P(C \mid X) = \frac{P(X \mid C) \cdot P(C)}{P(X)}$$ + +Thuật toán Naive Bayes tính xác suất của từng class cho một mẫu dữ liệu mới, và chọn class có xác suất cao nhất làm kết quả phân loại. Điểm quan trọng là giả định Naive Bayes đưa ra: các đặc trưng $$X_i$$ là độc lập có điều kiện dựa trên class $$C$$. Điều này cho phép công thức được viết lại dưới dạng: + +$$P(C \mid X_1, X_2, ..., X_n) \propto P(C) \cdot \prod_{i=1}^{n} P(X_i \mid C)$$ + +#### 2.2. Gaussian Naive Bayes + +Gaussian Naive Bayes (GNB) được sử dụng khi các feature là số và giả định rằng các đặc trưng này tuân theo phân phối Gauss. Với GNB, xác suất $$P(X_i \mid C)$$ được tính bằng hàm mật độ xác suất của phân phối Gaussian: + +$$P(X_i \mid C) = \frac{1}{\sqrt{2\pi \sigma_{iC}^2}}exp(-\frac{(X_i - \mu_{iC})^2}{2\sigma_{iC}^2})$$ + +Trong đó, $$\mu_{iC}$$ và $$\sigma_{iC}^2$$ là trung bình và phương sai của phân phối Gauss của feature đó trong class $$C$$. + +#### 2.3. Multinomial Naive Bayes + +Multinomial Naive Bayes (MNB) chủ yếu được sử dụng cho các bài toán phân loại văn bản, nơi dữ liệu được biểu diễn dưới dạng tần suất hoặc xác suất xuất hiện của các từ (hoặc đặc trưng) trong một tài liệu. Với MNB, xác suất $$P(X_i \mid C)$$ được tính dựa trên tần suất xuất hiện của từ $$X_i$$ trong các tài liệu thuộc class $$C$$: + +$$P(X_i \mid C) = \frac{N_{iC} + \alpha}{N_C + \alpha \vert V \vert}$$ + +Trong đó, + +\* $$N_{iC}$$ : tổng số lần xuất hiện của $$X_i$$ trong các tài liệu thuộc class $$C$$ + +\* $$N_C$$: tổng số từ trong các tài liệu ở class $$C$$ + +\* $$\vert V \vert$$: kích thước từ vựng + +\* $$\alpha$$: giá trị làm mượt để tránh bị probability = 0, thường sử dụng Laplace smoothing với $$\alpha = 1.$$ + +#### 2.4. Complement multinomial Naive Bayes + +Complement Naive Bayes (CNB) là một biến thể của MNB, đặc biệt hữu ích cho các tập dữ liệu mất cân bằng. Thay vì tính xác suất $$P(X_i \mid C)$$ trực tiếp cho một class, CNB tính xác suất của từ $X_i$ trong tất cả các class khác ngoài class $$C$$. Điều này giúp CNB hoạt động tốt hơn khi có sự chênh lệch lớn giữa các class. + +Công thức của CNB: + +$$P(X_i \mid C) = \frac{N_{i \overline{C}} + \alpha}{N_{\overline{C}} + \alpha|V|}$$ + +Trong đó, + +\* $$N_{i \overline{C}}$$: tổng số lần xuất hiện của $$X_i$$ trong các tài liệu **không** thuộc class $$C$$. + +\* $$N_{\overline{C}}$$: tổng số từ trong các tài liệu **không thuộc** class $$C$$. + +\* $$\vert V \vert$$: kích thước từ vựng. + +\* $$\alpha$$: giá trị làm mượt để tránh bị probability = 0, thường sử dụng Laplace smoothing với $$\alpha = 1.$$. + +### 3. Ưu và nhược + +**Ưu điểm** + +* Đơn giản và dễ triển khai + +* Không yêu cầu nhiều dữ liệu huấn luyện + +* Xử lý tốt cả dữ liệu số (numerical) và + +* Thời gian xử lý nhanh + +* Ít bị ảnh hưởng bởi các feature không liên quan (curse of dimensionality) + +**Nhược điểm** + +* Naive Bayes giả định rằng tất cả các đặc trưng (hoặc yếu tố dự báo) là độc lập, điều này hiếm khi xảy ra trong thực tế. Điều này giới hạn tính ứng dụng của thuật toán trong các trường hợp thực tế. + +* Các ước lượng của nó có thể không chính xác trong một số trường hợp, vì vậy không nên quá tin tưởng và dùng vào các giá trị xác suất mà nó trả về để tham dự các cuộc thi hoặc viết papers. + + +### 4. Kết luận + +Naive Bayes là một thuật toán đơn giản nhưng mạnh mẽ, đặc biệt hiệu quả với các bài toán phân loại văn bản và dữ liệu lớn. Tuy có những hạn chế về giả định độc lập và nhạy cảm với dữ liệu hiếm, các biến thể như Complement Naive Bayes có thể khắc phục phần nào những nhược điểm này, giúp mô hình phù hợp hơn cho các bài toán phức tạp và dữ liệu mất cân bằng. + + +### References +1\. [1.9. Naive Bayes - Scikit learn][sklearn_blog] +2\. [Complement naive bayes - Cross Validated][complement_naive_bayes] +3\. [Naïve Bayes Algorithm's Advantages and Disadvantages - Kaggle][kaggle_sharing] + +[sklearn_blog]: https://scikit-learn.org/stable/modules/naive_bayes.html +[complement_naive_bayes]: https://stats.stackexchange.com/questions/126009/complement-naive-bayes?fbclid=IwY2xjawFu5nlleHRuA2FlbQIxMAABHRLpkd8GUz2YboUH8UvGyJLjbqbFZ7MfNNNXZaTfMt81mnVjFWgz34cJvQ_aem_BrGkxexFczlf1946cjxcDg +[kaggle_sharing]: https://www.kaggle.com/discussions/getting-started/225022 \ No newline at end of file diff --git a/_posts/2024-05-18-gan.md b/_posts/2024-05-18-gan.md new file mode 100644 index 00000000000..ddc2fc88a97 --- /dev/null +++ b/_posts/2024-05-18-gan.md @@ -0,0 +1,290 @@ +--- +title: "Giải thích và code Generative Adversarial Networks" +mathjax: true +layout: post +categories: media +--- + + +### 1. Giới thiệu + +GANs (Generative Adversarial Networks) được giới thiệu lần đầu vào năm 2014 trong bài báo "[_Generative Adversarial Nets_][paper link]" của Ian J. Goodfellow và các cộng sự. Đây là một phương pháp học không giám sát (unsupervised learning) được sử dụng để tạo ra dữ liệu mới mang tính chân thực và đa dạng. + +
+Tất cả gương mặt trên đều được tạo bởi GANs +
Hình 1.1. Tất cả gương mặt trên đều được tạo bởi GANs
+
+ +Mô hình GANs có kiến trúc cực kỳ thú vị, bao gồm hai mạng chính: **Generator** và **Discriminator**, hoạt động theo cơ chế đối kháng. Generator cố gắng tạo ra dữ liệu giả mạo giống như dữ liệu thật, trong khi Discriminator cố gắng phân biệt giữa dữ liệu thật và dữ liệu giả. Qua quá trình huấn luyện, cả hai mạng này đều cải thiện chất lượng của mình, dẫn đến việc tạo ra dữ liệu giả ngày càng chân thực hơn. + + +
+ +
Hình 1.1. Ý tưởng của GANs
+
+ +Mô hình GANs hoạt động dựa trên cơ chế đối kháng giữa hai mạng neural: Generator (người tạo) và Discriminator (người kiểm tra). Hãy tưởng tượng bạn đang đào tạo một người làm tiền giả (Generator) và một người kiểm tra tiền giả (Discriminator). Người làm tiền giả cố gắng tạo ra những tờ tiền giả trông giống như thật nhất có thể. Trong khi đó, người kiểm tra tiền sẽ cố gắng phân biệt giữa tờ tiền thật và tờ tiền giả. + +Ban đầu, người làm tiền giả chưa có kỹ năng, nên tờ tiền giả trông rất khác so với tờ tiền thật, và người kiểm tra tiền dễ dàng phát hiện. Tuy nhiên, qua mỗi lần bị phát hiện, người làm tiền giả học hỏi từ những sai lầm và cải thiện kỹ năng của mình, tạo ra những tờ tiền giả ngày càng giống thật hơn. Đồng thời, người kiểm tra tiền cũng học hỏi và nâng cao khả năng phát hiện tiền giả. + +Quá trình này tiếp diễn cho đến khi người làm tiền giả trở nên rất giỏi trong việc tạo ra những tờ tiền giả mà ngay cả người kiểm tra tiền cũng khó phát hiện. Kết quả cuối cùng là một mô hình Generator có khả năng tạo ra dữ liệu giả rất chân thực, tương tự như dữ liệu thật. + +### 2. Toán của GANs + +Ý tưởng chính của GANs đã được đề cập ở phần trên, bây giờ chúng ta hãy cùng đi vào phần toán để nắm bắt được cách hoạt động chi tiết của kiến trúc thú vị này nhé. + +$$\min_{G} \max_{D} V(D, G) = \mathop{\mathbb{E}}_{x \sim p_{data}(x)[log(D(x))]} + \mathop{\mathbb{E}}_{z \sim p_{z}(z)[log(1 - D(G(z)))]}$$ + +$$V: Value function$$ + +$$D: Discriminator$$ + +$$G: Generator$$ + +Theo như công thức tác giả đề cập, thì mô hình **discriminator muốn maximise value function** trong khi **generator tìm cách minimise** nó. Ở vế trái của phương trình là expectation của mô hình Discriminator trên real data, tức là nếu discrimator nhận diện đúng càng nhiều các real data thì giá trị này càng lớn. + +$$\mathop{\mathbb{E}}_{x \sim p_{data}(x)[log(D(x))]}: \text{High -> Good Discriminator}$$ + +$$\mathop{\mathbb{E}}_{x \sim p_{data}(x)[log(1 - D(x))]}: \text{High -> Bad Discriminator}$$ + +$$\mathop{\mathbb{E}}_{z \sim p_{z}(z)[log(1 - D(G(z)))]}: \text{High -> Good Discriminator}$$ + +$$\mathop{\mathbb{E}}_{z \sim p_{z}(z)[log(D(G(z)))]}: \text{High -> Bad Discriminator}$$ + + +### 3. Implementation + +Ở phần này, chúng ta sẽ implement một kiến trúc GAN đơn giản trên tập MNIST digits để xem chất lượng hình ảnh được sinh ra như thế nào nhé. + +* **Step 1: Import các thư viện cần thiết** + +```python +import torch +from torch import nn, optim +import numpy as np +import matplotlib.pyplot as plt +from torch.utils.data import DataLoader, Dataset +from torchvision.datasets import MNIST +import torchvision +import torch.nn.functional as F +from tqdm import tqdm +import torchvision.utils as vutils + +%matplotlib inline +``` + +* **Step 2: Load dataset** + +```python +transform = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor() + ]) + +BATCH_SIZE = 64 +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +train_loader = DataLoader(MNIST("./data/", train=True, download=True, transform=transform), batch_size = BATCH_SIZE, shuffle=True) +``` + +* **Step 3: Xây dựng mô hình Generator và Discriminator** + +Đầu tiên, ta sẽ xây dựng khối convolution gồm Conv + Batch Norm + ReLU +```python +class Conv(nn.Module): + def __init__(self, in_channels: int, out_channels: int, + kernel_size: int = 3, stride: int = 1, padding: int =1): + super().__init__() + self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False) + self.bn = nn.BatchNorm2d(out_channels) + self.act = nn.ReLU() + + self.block = nn.Sequential(self.conv, self.bn, self.act) + + def forward(self, x): + return self.block(x) +``` + +Sau khi có khối Conv, ta dùng nó để xây dựng discriminator và generator + +```python +class Discriminator(nn.Module): + def __init__(self): + super().__init__() + self.conv1 = Conv(1, 16, 3, 1, 1) + self.conv2 = Conv(16, 32, 3, 1, 1) + self.conv3 = Conv(32, 64, 3, 1, 1) + self.pooling = nn.MaxPool2d(2, 2) + + self.fc1 = nn.Linear(7*7*64, 1) + + + def forward(self, x: torch.Tensor): + x = self.conv1(x) + x = self.pooling(x) + + x = self.conv2(x) + x = self.pooling(x) + + x = self.conv3(x) + + x = x.flatten(1) + + x = self.fc1(x) + x = nn.Sigmoid()(x) + return x +``` + +```python +class Generator(nn.Module): + def __init__(self, z_dim: int = 20): + super().__init__() + self.fc1 = nn.Linear(z_dim, 7*7*256) + + self.conv1 = Conv(256, 128, 3, 1, 1) + self.conv2 = Conv(128, 64, 3, 1, 1) + self.conv3 = nn.Conv2d(64, 1, 1, 1, bias=True) + self.upsample = nn.UpsamplingBilinear2d(scale_factor=2) + + def forward(self, x: torch.Tensor): + x = self.fc1(x) + x = x.reshape(-1, 256, 7, 7) + + x = self.upsample(x) + x = self.conv1(x) + + x = self.upsample(x) + x = self.conv2(x) + x = self.conv3(x) + x = nn.Sigmoid()(x) + + return x +``` + +* **Step 4: Thiết lập optimizer, epochs, learning rate, ...** + +```python +Z_DIM = 100 +disc = Discriminator().to(DEVICE) +generator = Generator(Z_DIM).to(DEVICE) + +GEN_LR = 1e-3 +DISC_LR = 1e-3 + +gen_optimizer = optim.Adam(generator.parameters(), lr=GEN_LR, amsgrad=True) +disc_optimizer = optim.Adam(disc.parameters(), lr = DISC_LR, amsgrad=True) + +loss_fn = nn.BCELoss() +``` + +Thử sinh ra một vài tấm ảnh trước khi train để xem nó nhìn như thế nào nhé. + +```python +sample_batch = torch.randn((5, Z_DIM)) + +generated_data = generator(sample_batch) + +fig, ax = plt.subplots(1, 5, figsize=(15, 15)) +for i in range(5): + ax[i].imshow(generated_data.detach().cpu()[i][0]) + ax[i].set_title(f"Sample {i}") +``` +
+ +
Hình 3.1. Data được sinh ra trước khi train model
+
+ +* **Step 5: Viết các hàm training và visualise** + +* * Hàm để training cho 1 epoch + +```python +def train_1_epoch(loader): + gen_loss_value = 0 + disc_loss_value = 0 + + for real_data, _ in tqdm(train_loader): + + z = torch.randn((BATCH_SIZE, Z_DIM)).to(DEVICE) + generated_data = generator(z) + real_data = real_data.to(DEVICE) + + disc_real_data = disc(real_data) + disc_fake_data = disc(generated_data.detach()) + # Real data: 0 Fake data: 1 + disc_loss = loss_fn(disc_fake_data, torch.ones_like(disc_fake_data)) + loss_fn(disc_real_data, torch.zeros_like(disc_real_data)) + + disc_optimizer.zero_grad() + disc_loss.backward(retain_graph=True) + disc_optimizer.step() + + disc_fake_data = disc(generated_data) + gen_loss = loss_fn(disc_fake_data, torch.zeros_like(disc_fake_data)) + + gen_optimizer.zero_grad() + gen_loss.backward() + gen_optimizer.step() + + gen_loss_value += gen_loss.item() + disc_loss_value += disc_loss.item() + + return disc_loss_value, gen_loss_value +``` + +* * Hàm để visualise + +```python +@torch.no_grad() +def visualise(): + generator.eval() + z = torch.randn((16, Z_DIM)).to(DEVICE) + generated_data = generator(z).cpu() + + fig, ax = plt.subplots(figsize=(10, 10)) + plt.axis("off") + ax.imshow(np.transpose(vutils.make_grid(generated_data, padding=2, normalize=True), (1, 2, 0))) + plt.show() + generator.train() +``` + +* * Và cuối cùng là hàm train cho toàn bộ quá trình + +```python +def train(epochs: int): + for i in range(1, epochs+1): + disc_loss_value, gen_loss_value = train_1_epoch(train_loader) + print(f"Epoch: {i} Discriminator Loss: {disc_loss_value} Generator Loss: {gen_loss_value}") + visualise() +``` + +* **Step 6: Train model và quan sát** + +Ta train model với khoảng 500 epochs + +```python +train(500) +``` + +
+ +
Hình 3.2. Data được sinh ra sau khi train model sau 90 epochs
+
+ + +**Lưu ý**: Quá trình training GANs cực kì khó hội tụ và cần rất nhiều tài nguyên và thời gian để có thể tự train ra một mô hình sinh nhìn thật. Vì thế, mình khuyên các bạn dùng các pre-trained models của các công ty hay tổ chức lớn và fine-tune lại với dataset riêng thay vì train from scratch. + +### 4. Kết luận + +Qua bài viết này, chúng ta đã hiểu rõ về cấu trúc và cơ chế hoạt động của GANs, cũng như cách triển khai một mô hình GAN đơn giản để tạo ra hình ảnh từ tập dữ liệu MNIST. GANs là một công cụ mạnh mẽ trong việc tạo ra dữ liệu giả có chất lượng cao, được ứng dụng rộng rãi trong nhiều lĩnh vực như tạo ảnh, video, âm thanh và nhiều dạng dữ liệu khác. Việc nắm vững GANs sẽ mở ra nhiều cơ hội trong nghiên cứu và ứng dụng thực tế. + +### References + +1\. [Generative Adversarial Nets - arXiv][paper link] + +2\. [DCGAN implementation from scratch][Aladdin Persson Youtube] + + + + + +[paper link]: https://arxiv.org/pdf/1406.2661 +[Aladdin Persson Youtube]: https://www.youtube.com/watch?v=IZtv9s_Wx9I&t=1054s \ No newline at end of file diff --git a/_posts/2024-05-18-variational_autoencoder.md b/_posts/2024-05-18-variational_autoencoder.md new file mode 100644 index 00000000000..bd6e219b31d --- /dev/null +++ b/_posts/2024-05-18-variational_autoencoder.md @@ -0,0 +1,339 @@ +--- +title: "Giải thích và code Variational AutoEncoder" +mathjax: true +layout: post +--- + +Xin chào các bạn! + +Trong bài post này, mình sẽ giới thiệu về mô hình Variational AutoEncoder được công bố trong paper "[_Auto-Encoding Variational Bayes_][vae_paper]" của tác giả Diederik P. Kingma và các cộng sự. Đây là một kiến trúc tương tự như AutoEncoder và có thêm thành phần stochastic trong phần bottleneck để khiến cho nó có khả năng tạo ra những data mới lạ (tuy vẫn thuộc distribution của training dataset). + + +### 1. Tổng quan + +Được chính thức published trong paper "_Auto-Encoding Variational Bayes_" vào năm 2014, Variational AutoEncoder đã tạo ra tiếng vang rất lớn và tạo ra một cú hích giúp ngày càng nhiều probabilistic models được nghiên cứu hơn. Trong paper của mình, tác giả đề cập tới kiến trúc tổng quan của mô hình, các công thức toán học chứng minh, và một vài tricks để làm cho VAE có thể hoạt động được. Mình nói tricks để giúp cho model hoạt động được bởi nếu không có trick này thì chắc chắn VAE sẽ không work và các probabilistic models sau này cũng sẽ không phát triển mạnh như bây giờ. + +Về cơ bản, VAE được dùng để tạo ra các dữ liệu mới từ tập data training. Ngoài ra, mô hình còn giải quyết điểm yếu cố hữu của mô hình Auto-Encoder truyền thống trong việc generate data. Một chút về nhược điểm về Auto-Encoder, mô hình này bị ván đề **không sinh ra được data đa dạng và không có tính liên tục trong latent space**. Với sự ra mắt của VAE, 2 nhược điểm này đã được khắc phục. + +
+ +
Hình 1.1. Ảnh được sinh ra từ VAE
+
+ +### 2. Kiến trúc + +
+ +
Hình 2.1. Kiến trúc tổng quan của VAE
+
+ +Về cơ bản, kiến trúc VAE giống gần như 99% với Auto-Encoder. VAE cũng bao gồm encoder, bottleneck, decoder, tuy nhiên có một sự khác biệt ở bottleneck. Đó là ở bottleneck của VAE, nó sẽ có 2 output là mean và standard deviation thay vì chỉ có một output bottleneck như Auto-Encoder. Tuy nhiên, 2 khối này sẽ được kết hợp lại bằng các element-wise operations để đưa vào decoder block tương tự như auto-encoder. + +### 3. Giải thích toán học + +VAE bao gồm 2 khối: Encoder, và Decoder. Khối encoder có nhiệm vụ nén và mã hóa thông tin trong khi khối decoder có nhiệm vụ khôi phục lại thông tin từ thông tin từ encoder. Việc này có thể được mô tả bằng toán học như sau: + +$$\text{Encoder: } p(z|x) = \frac{p(x|z)p(z)}{p(x)}$$ + +$$\text{Decoder: } p(x|z) = \frac{p(z|x)p(x)}{p(z)}$$ + +Đáng tiếc, 2 công thức trên đều rất khó để tính toán vì các priors. + +Cụ thể, + +$$p(x) = \int p(x|z)p(z)dz$$ + +$$p(z) = \int p(z|x)p(x)dx$$ + +Nhìn vào 2 công thức trên, chúng ta có quan sát như sau. Để tính đươc p(x), ta phải quét qua toàn bộ các giá trị của latent space và để tính được p(z) ta phải quét qua toàn bộ các giá trị của data x. Nếu latent space và dataset của chúng ta > 1-D, điều này có thể khả thi, tuy nhiên chúng ta thường encode nó trong không gian lớn hơn rất nhiều so với 1D, vậy cần phải có cách khác. + +Cách mà tác giả đề xuất trong paper là dùng một hàm xấp xỉ (approximator) để tính các giá trị latent (z) và reconstructed data ($$\hat{x}$$). Cụ thể, chúng sẽ có biểu thức toán học như sau: + +$$\text{Encoder: } p(z|x) = \frac{p(x|z)p(z)}{p(x)} \text{ (intractable)} => q_{\theta}(z|x)$$ + +$$\text{Decoder: } p(x|z) = \frac{p(z|x)p(x)}{p(z)} \text{ (intractable)} => q_{\phi}(x|z)$$ + +Trong công thức tính của Encoder đã bao gồm Decoder và ngược lại, vì vậy chúng ta chỉ cần tối ưu một trong hai khối và khối còn lại sẽ tự động được tối ưu. Trong paper của tác giả, họ chọn tối ưu Encoder nên trong post này mình cũng sẽ sử dụng Encoder để align với tác giả. + +Vì chúng ta muốn hàm Encoder +$$q_{\theta}(z|x)$$ + xấp xỉ giống y hệt như hàm intractable +$$p(z|x)$$ + nên ta sẽ có hàm mục tiêu như sau: + +$$\underset{\theta, \phi}{\text{Minimise }} D_{KL}(q_{\theta}(z|x)||p(z|x)) \text{ (1)}$$ + +$$\underset{\theta, \phi}{\text{Minimise }} \mathbb{E}_{q_{\theta}(z|x)}[log(\frac{q_{\theta}(z|x)}{p(z|x)})] \text{ (2)}$$ + +$$\underset{\theta, \phi}{\text{Minimise }} \mathbb{E}_{q_{\theta}(z|x)}[log(q_{\theta}(z|x)) - log(p(x, z))] + log(p(x)) \text{ (3)}$$ + +$$p(x)$$ +có thể bị lược ra khỏi hàm loss vì nó không có params tối ưu và không có công dụng nào. Tuy nhiên, ở phần dưới sẽ có một trường hợp không có params tối ưu nhưng vẫn được giữ lại. + +$$\underset{\theta, \phi}{\text{Minimise }} \mathbb{E}_{q_{\theta}(z|x)}[log(q_{\theta}(z|x)) - log(p_{\phi}(x|z)) - log(p(z))] \text{ (4)}$$ + +$$\underset{\theta, \phi}{\text{Minimise }}\mathbb{E}_{q_{\theta}(z|x)}[log(\frac{q_{\theta}(z|x)}{p(z)}) - log(p_{\phi}(x|z))] \text{ (5)}$$ + +$$\underset{\theta, \phi}{\text{Minimise }} \mathbb{E}_{q_{\theta}(z|x)}[-log(p_{\phi}(x|z))] + D_{KL}(q_{\theta}(z|x)||p(z)) \text{ (6)}$$ + +Ta có gradient của phương trình trên với 2 biến $$\phi$$ và $$\theta$$ như sau: + +$$\nabla_{\phi}\mathbb{E}_{q_{\theta}(z|x)}[-log(p_{\phi}(x|z))] + D_{KL}(q_{\theta}(z|x)||p(z)) = \nabla_{\phi}-log(p_{\phi}(x|z)) \text{ (7)}$$ + +$$\nabla_{\theta}\mathbb{E}_{q_{\theta}(z|x)}[-log(p_{\phi}(x|z))] + D_{KL}(q_{\theta}(z|x)||p(z)) ≠ \nabla_{\theta}D_{KL}(q_{\theta}(z|x)||p(z)) \text{ (8)}$$ + +Ở phương trình (7), gradient có thể được tính dễ dàng nhờ vào Monte Carlo. Tuy nhiên, ở phương trình (8), ta không thể xấp xỉ bằng Monte Carlo do vướng phải parameter $$\theta$$ ở phần expectation. Nếu phần phân phối tính expectation dựa vào $$\theta$$ được thay bằng một biến nào đó không liên quan thì phương trình (8) sẽ đơn giản trở thành tối ưu KL divergence. + +Để giải quyết vấn đề này, tác giả của paper đã đề xuất reparameterisation trick. Ý tưởng của phương pháp này như sau: + +Giả sử ta có một biến $$z \sim N(\mu, \sigma^2)$$, ta có thể biểu diễn nó như sau: + +$$z = \mu + \sigma^2 * \epsilon$$ + +$$\text{Với } \epsilon \sim N(\mathbf{0}, I)$$ + +Dựa vào reparameterisation trick, ta sẽ áp dụng như sau: + +$$z = g_{\theta}(x, \epsilon) = \mu_{\theta}(x) + \sigma_{\theta}^2(x) * \epsilon$$ + +$$\text{Với } \epsilon \sim N(\mathbf{0}, I)$$ + +
+ +
Hình 3.1. Minh họa cách hoạt động của Reparameterisation trick
+
+ +Áp dụng reparameterisation trick, ta biến đổi lại phương trình (8) như sau: + +$$\nabla_{\theta}\mathbb{E}_{p(\epsilon)}[-log(p_{\phi}(x|z = g_{\theta}(x, \epsilon)))] + D_{KL}(q_{\theta}(z|x)||p(z)) ≠ \nabla_{\theta}D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon)|x)||p(z)) \text{ (9)}$$ + +Với Expectation không còn $$\theta$$ trong (9), chúng ta có thể tính gradient một cách dễ dàng như sau: + +$$\nabla_{\theta}\mathbb{E}_{p(\epsilon)}[-log(p_{\phi}(x|z = g_{\theta}(x, \epsilon)))] + D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon))||p(z)) = \nabla_{\theta}D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon))||p(z)) \text{ (10)}$$ + +Tới đây, chúng ta đã có được phương trình để tính gradient cập nhật trọng số cho model. + +Tóm lại, chúng ta sẽ cần phương trình (7) và (10) để cập nhật các parameters trong mạng. Mình sẽ viết lại 2 phương trình sau khi được áp dụng reparameterisation trick. + +$$\nabla_{\phi}\mathbb{E}_{p(\epsilon)}[-log(p_{\phi}(x|z = g_{\theta}(x, \epsilon)))] + D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon))||p(z)) = \nabla_{\phi}-log(p_{\phi}(x|z = g_{\theta}(x, \epsilon))) \text{ (7)}$$ + +$$\nabla_{\theta}\mathbb{E}_{p(\epsilon)}[-log(p_{\phi}(x|z = g_{\theta}(x, \epsilon)))] + D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon))||p(z)) = \nabla_{\theta}D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon))||p(z)) \text{ (10)}$$ + +Phương trình (7) chỉ đơn giản là tối ưu các tham số của decoder sao để cho reconstructed data giống với data gốc. Chúng ta có thể dùng L1, L2, Binary Cross Entropy, ... Tuy nhiên, đối với phương trình (10), chúng ta phải sample từ prior $$p(z)$$ và $$\epsilon \sim N(\mathbf{0}, \mathbf{I})$$, và việc sample mỗi iteration mỗi khác như vậy sẽ khiến **việc training khó hội tụ hơn**, vì thế cần một cách thức khác để có thể tối ưu nó mà không cần sample từ các distributions. + +Chúng ta sẽ assume latent space z là một phân phối Gaussian và prior của nó là một phân bố Gauss tiêu chuẩn $$p(z) = N(0, I)$$ và posterior $$q_{\theta}(z = g_{\theta}(x, \epsilon)) = N(\mu_{\theta}(x), \sigma_{\theta}^2(x))$$. + +Vì vậy, chúng ta có thể biến đổi (10) thành: + +$$\nabla_{\theta}D_{KL}(q_{\theta}(z = g_{\theta}(x, \epsilon))||p(z)) = D_{KL}(N(\mu_{\theta}(x), \sigma_{\theta}^2(x))|| N(0, I)) \text{ (11)}$$ + + +Theo công thức Gauss, ta có: + +$$q_{\theta}(z = g_{\theta}(x, \epsilon)) = \frac{1}{\sigma \sqrt {2\pi} } e^{-0.5(\frac{z - \mu_{\theta}(x)}{\sigma_{\theta}(x)})^2}$$ + +$$p(z) = \frac{1}{\sqrt {2\pi}}e^{-0.5z^2}$$ + +Từ 2 công thức Gauss trên, ta áp dụng vào để tính khoảng cách Kullback-Leibler một cách deterministic. + +$$D_{KL}(N(\mu_{\theta}(x), \sigma_{\theta}^2(x))|| N(0, I)) = \mathbb{E}_{q_{\theta}}[log(q_{\theta}) - log(p(z))]$$ + +$$\mathbb{E}_{q_{\theta}}[log(\frac{1}{\sigma_{\theta} \sqrt{2 \pi}}) - \frac{1}{2}(\frac{z - \mu_{\theta}}{\sigma_{\theta}})^2 - log(\frac{1}{\sqrt{2 \pi}}) + \frac{1}{2} z^2]$$ + +$$\mathbb{E}_{q_{\theta}}[log(\frac{1}{\sigma_{\theta} \sqrt{2 \pi}}) - log(\frac{1}{\sqrt{2 \pi}})] + \mathbb{E}_{q_{\theta}}[- \frac{1}{2}(\frac{z - \mu_{\theta}}{\sigma_{\theta}})^2] + \mathbb{E}_{q_{\theta}}[\frac{1}{2} z^2]$$ + +Trong đó: + +$$\mathbb{E}_{q_{\theta}}[log(\frac{1}{\sigma_{\theta} \sqrt{2 \pi}}) - log(\frac{1}{\sqrt{2 \pi}})] = - \frac{1}{2}log(\sigma_{\theta})$$ + +$$\mathbb{E}_{q_{\theta}}[- \frac{1}{2}(\frac{z - \mu_{\theta}}{\sigma_{\theta}})^2] = - \frac{1}{2}$$ + +$$\mathbb{E}_{q_{\theta}}[\frac{1}{2} z^2] = \frac{1}{2}(\sigma_{\theta}^2 + \mu_{\theta}^2)$$ + +Vì vậy, tổng hợp lại ta có được là: + +$$D_{KL}(N(\mu_{\theta}(x), \sigma_{\theta}^2(x))|| N(0, I)) = \frac{1}{2}[-log(\sigma_{\theta}^2) - 1 + \sigma_{\theta}^2 + \mu_{\theta}^2]$$ + + +### 4. Code VAE với Python và Keras + +Ở phần này, một mô hình VAE đơn giản sẽ được xây dựng và train với ngôn ngữ Python và thư viện Keras để kiểm chứng lý thuyết. + +* **Bước 1**: Import các thư viện cần thiết + +```python + +import os + +os.environ["KERAS_BACKEND"] = "tensorflow" + +import numpy as np +import tensorflow as tf +import keras +from keras import ops +from keras import layers +import matplotlib.pyplot as plt +``` + +* **Bước 2**: Model + + +```python + +# Sampling step from mean and var +class Sampling(layers.Layer): + """Uses (z_mean, z_log_var) to sample z, the vector encoding a digit.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.seed_generator = keras.random.SeedGenerator(1337) + + def call(self, inputs): + z_mean, z_log_var = inputs + batch = ops.shape(z_mean)[0] + dim = ops.shape(z_mean)[1] + epsilon = keras.random.normal(shape=(batch, dim), seed=self.seed_generator) + return z_mean + ops.exp(0.5 * z_log_var) * epsilon + +# Encoder +latent_dim = 2 + +encoder_inputs = keras.Input(shape=(28, 28, 1)) +x = layers.Conv2D(32, 3, activation="relu", strides=2, padding="same")(encoder_inputs) +x = layers.Conv2D(64, 3, activation="relu", strides=2, padding="same")(x) +x = layers.Flatten()(x) +x = layers.Dense(16, activation="relu")(x) +z_mean = layers.Dense(latent_dim, name="z_mean")(x) +z_log_var = layers.Dense(latent_dim, name="z_log_var")(x) +z = Sampling()([z_mean, z_log_var]) +encoder = keras.Model(encoder_inputs, [z_mean, z_log_var, z], name="encoder") + +# Decoder +latent_inputs = keras.Input(shape=(latent_dim,)) +x = layers.Dense(7 * 7 * 64, activation="relu")(latent_inputs) +x = layers.Reshape((7, 7, 64))(x) +x = layers.Conv2DTranspose(64, 3, activation="relu", strides=2, padding="same")(x) +x = layers.Conv2DTranspose(32, 3, activation="relu", strides=2, padding="same")(x) +decoder_outputs = layers.Conv2DTranspose(1, 3, activation="sigmoid", padding="same")(x) +decoder = keras.Model(latent_inputs, decoder_outputs, name="decoder") +``` + +* **Bước 3**: Training and loss functions + +```python +class VAE(keras.Model): + def __init__(self, encoder, decoder, **kwargs): + super().__init__(**kwargs) + self.encoder = encoder + self.decoder = decoder + self.total_loss_tracker = keras.metrics.Mean(name="total_loss") + self.reconstruction_loss_tracker = keras.metrics.Mean( + name="reconstruction_loss" + ) + self.kl_loss_tracker = keras.metrics.Mean(name="kl_loss") + + @property + def metrics(self): + return [ + self.total_loss_tracker, + self.reconstruction_loss_tracker, + self.kl_loss_tracker, + ] + + def train_step(self, data): + with tf.GradientTape() as tape: + z_mean, z_log_var, z = self.encoder(data) + reconstruction = self.decoder(z) + reconstruction_loss = ops.mean( + ops.sum( + keras.losses.binary_crossentropy(data, reconstruction), + axis=(1, 2), + ) + ) + kl_loss = -0.5 * (1 + z_log_var - ops.square(z_mean) - ops.exp(z_log_var)) + kl_loss = ops.mean(ops.sum(kl_loss, axis=1)) + total_loss = reconstruction_loss + kl_loss + grads = tape.gradient(total_loss, self.trainable_weights) + self.optimizer.apply_gradients(zip(grads, self.trainable_weights)) + self.total_loss_tracker.update_state(total_loss) + self.reconstruction_loss_tracker.update_state(reconstruction_loss) + self.kl_loss_tracker.update_state(kl_loss) + return { + "loss": self.total_loss_tracker.result(), + "reconstruction_loss": self.reconstruction_loss_tracker.result(), + "kl_loss": self.kl_loss_tracker.result(), + } +``` + +* **Bước 4**: Training + +```python +(x_train, _), (x_test, _) = keras.datasets.mnist.load_data() +mnist_digits = np.concatenate([x_train, x_test], axis=0) +mnist_digits = np.expand_dims(mnist_digits, -1).astype("float32") / 255 + +vae = VAE(encoder, decoder) +vae.compile(optimizer=keras.optimizers.Adam()) +vae.fit(mnist_digits, epochs=30, batch_size=128) +``` + +* **Bước 5**: Visualise kết quả + +```python +def plot_latent_space(vae, n=30, figsize=15): + # display a n*n 2D manifold of digits + digit_size = 28 + scale = 1.0 + figure = np.zeros((digit_size * n, digit_size * n)) + # linearly spaced coordinates corresponding to the 2D plot + # of digit classes in the latent space + grid_x = np.linspace(-scale, scale, n) + grid_y = np.linspace(-scale, scale, n)[::-1] + + for i, yi in enumerate(grid_y): + for j, xi in enumerate(grid_x): + z_sample = np.array([[xi, yi]]) + x_decoded = vae.decoder.predict(z_sample, verbose=0) + digit = x_decoded[0].reshape(digit_size, digit_size) + figure[ + i * digit_size : (i + 1) * digit_size, + j * digit_size : (j + 1) * digit_size, + ] = digit + + plt.figure(figsize=(figsize, figsize)) + start_range = digit_size // 2 + end_range = n * digit_size + start_range + pixel_range = np.arange(start_range, end_range, digit_size) + sample_range_x = np.round(grid_x, 1) + sample_range_y = np.round(grid_y, 1) + plt.xticks(pixel_range, sample_range_x) + plt.yticks(pixel_range, sample_range_y) + plt.xlabel("z[0]") + plt.ylabel("z[1]") + plt.imshow(figure, cmap="Greys_r") + plt.show() + + +plot_latent_space(vae) +``` + +
+ +
Hình 4.1. Visualisation của các samples được sinh ra từ VAE
+
+ +### 5. Kết luận + +VAE là một kiến trúc tạo data với cơ chế stochastic, điều này cho khả năng tạo ra những samples khác hơn so với data gốc. Tuy nhiên, sự khác biệt này sẽ không cực kì đáng kể và chất lượng sẽ khó có thể so sánh với GAN, hoặc Diffusion models. Tuy nhiên, đây vẫn là một kiến trúc rất tốt để học học vì các kiến trúc ra sau áp dụng stochastic đa số đều dựa trên reparameterisation trick của VAE. + +Trong bài, mình đã giới thiệu về VAE, các công thức toán để chứng minh VAE hoạt động, và cũng như code nó với keras và tensorflow. Hy vọng các bạn thấy hữu ích và đừng ngần ngại gửi feedback vào email mình nếu chưa hiểu hoặc thấy sai sót nhé. Chúc các bạn học tốt. + + +### References +1\. [Auto-Encoding Variational Bayes - arXiv][vae_paper] +2\. [Variational Autoencoder - Blog][nxaq_blog] +3\. [Keras implementation of VAE - Keras][keras_implementation] + +[vae_paper]: https://arxiv.org/abs/1312.6114 +[nxaq_blog]: https://anhquannguyen21.github.io/2022-03-12-Variational-Autoencoder/ +[keras_implementation]: https://keras.io/examples/generative/vae/ \ No newline at end of file diff --git a/_posts/2024-05-18-wgan.md b/_posts/2024-05-18-wgan.md new file mode 100644 index 00000000000..baac22d7811 --- /dev/null +++ b/_posts/2024-05-18-wgan.md @@ -0,0 +1,480 @@ +--- +title: "Giải thích và code WGAN" +mathjax: true +layout: post +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ giải thích về các công thức toán học Wasserstein GAN (WGAN) và code kiến trúc này from scratch với Pytorch. + +### 1. Giới thiệu + +WGAN là một kiến trúc tạo sinh (generative model) giống như GAN truyền thống với một vài sự thay đổi về hàm loss để cải thiện performance. Như các bạn đã biết, việc huấn luyện GAN truyền thống sẽ gặp rất nhiều khó khăn như collapse mode, non-convergence, diminished gradient, unbalance training, sensitive to hyperparameters, ... Các bạn có thể đọc thêm bài viết của Jonathan Hui để biết thêm chi tiết về các nhược điểm của GANs truyền thống, trong post của tác giả có liệt kê đầy đủ và giải thích chi tiết các nhược điểm cố hữu của GANs truyền thống. + +
+ +
+ + +### 2. Ưu điểm của WGAN so với GANs truyền thống + +#### 2.1. Wasserstein distance được tính như thế nào ? + +Khoảng cách Wasserstein hay còn được gọi là Earth Mover's distance (EMD), là một thước đo sự khác biệt giữa hai phân phối xác suất. Wasserstein distance đo lường chi phí tối thiểu để biến một phân phối xác suất này thành phân phối xác suất khác, trong đó "chi phí" được tính bằng cách nhân khoảng cách cần di chuyển với khối lượng di chuyển. + +
+ +
+ +Giả sử như sau, bạn có 100 cái bánh trung thu đặt ở 4 quận: quận 1, quận 2, quận 3, quận 4 và muốn vận chuyển số bánh ở 4 quận đó sang 3 quận khác là quận 5, quận 6, và quận 7. + +Vì chi phí vận chuyển dựa vào 2 yếu tố: cân nặng và quãng đường. Ta thiết lập bảng sau để đánh giá chi phí + +Bảng giá chi phí quãng đường (kilometers) + +| Xuất phát / Điểm đến | Quận 1 | Quận 2 | Quận 3| Quận 4| +|----------|----------|----------|---------|------| +| **Quận 5** | 5.2 | 20.2 | 4.7 | 6.6 | +| **Quận 6** | 12.2 | 24.8 | 9.1 | 11.3 | +| **Quận 7** | 7.3 | 20.2 | 9.4 | 6.1 | + + +Phương pháp 1, ta chuyển 10 bánh ở quận 1 đến quận 5 và tất cả bánh ở quận 2 qua quận 5 cho đủ yêu cầu của quận 5. Tiếp theo, ta dùng 45 bánh ở quận 3 để đắp vào đủ số bán cho quận 6, số dư còn lại ta chuyển cho quận 7. Sau cùng, ta chuyển hết 20 bánh ở quận 4 cho quận 7 là xong. Diễn giải bằng ma trận, ta sẽ trình bày như sau. + + +| Xuất phát / Điểm đến | Quận 1 (10 bánh) | Quận 2 (5 bánh) | Quận 3 (65 bánh)| Quận 4 (20 bánh)| +|----------|----------|----------|---------|------| +| **Quận 5 (15 bánh)** | 10 | 5 | 0 | 0 | +| **Quận 6 (45 bánh)** | 0 | 0 | 45 | 0 | +| **Quận 7 (40 bánh)** | 0 | 0 | 20 | 20 | + + +Phương pháp 2, ta có thể vận chuyển như sau: + + +| Xuất phát / Điểm đến | Quận 1 (10 bánh) | Quận 2 (5 bánh) | Quận 3 (65 bánh)| Quận 4 (20 bánh)| +|----------|----------|----------|---------|------| +| **Quận 5 (15 bánh)** | 5 | 5 | 5 | 0 | +| **Quận 6 (45 bánh)** | 5 | 0 | 30 | 10 | +| **Quận 7 (40 bánh)** | 0 | 0 | 30 | 10 | + +Với phương pháp 1, ta có chi phí được tính như sau: + +$$\text{cost} = 5.2 \times 10 + 20.2*5 + 9.1 \times 45 + 9.4 \times 20 + 6.1 \times 20 = 872.5$$ + +Với phương pháp 2, ta có chi phí được tính toán như sau: + +$$\text{cost} = 5.2 \times 5 + 12.2 \times 5 + 20.2 \times 5 + 4.7 \times 5 + 9.1 \times 30 + 9.4 \times 30 + 11.3 \times 10 + 6.1 \times 10 = 940.5$$ + +**Note**: Nếu tính tổng theo cột, ta sẽ có được tổng bánh của từng quận từ 1 tới 4. Ngược lại, nếu tính tổng theo hàng, ta sẽ có được tổng bánh của tứng quận từ 5 tới 7. Đây là **một điều kiện quan trọng để phần dưới dùng để biến đổi**. Đây còn gọi là marginal distributions. + +Với ví dụ minh họa trên, ta thấy với mỗi lời giải ta sẽ có kết quả khác nhau và lời giải mà cho kết quả cost nhỏ nhất được định nghĩa là **Wasserstein distance** . Vì vậy, khoảng cách Wasserstein distance có định nghĩa toán học như sau: + +$$\mathbf{W}_p(\mu, \upsilon) = (\inf_{\gamma \sim \Gamma(x, y)} \int_{\chi \times \chi} {d(x, y)^p d\gamma(x, y)})^{\frac{1}{p}} \quad \in \mathbb{R}^+$$ + +Với, + +$$\inf: \quad \text{infinum (Giá trị nhỏ nhất)}$$ + + +$$\chi \times \chi: \quad \text{joint probability của 2 phân phối} $$ + + +$$\gamma: \quad \text{Transport plan}$$ + + +$$\Gamma (x, y): \quad \text{Tập hợp tất cả các transport plans}$$ + + +$$d(x, y): \quad \text{Chi phí khoảng cách}$$ + + +$$d\gamma(x, y): \quad \text{Chi phí khối lượng}$$ + +#### 2.2. Tại sao hàm loss của GANs truyền thống là khoảng cách Jensen-Shannon giữa phân phối của tập dữ liệu gốc và dữ liệu sinh ? + +Đầu tiên, ta có hàm loss của GANs được định nghĩa như sau: + +$$L(D, G) = \mathbb{E}_{x \sim P_r(x)}[log(D(x))] + \mathbb{E}_{z \sim P_z(z)}[log(1 - D(G(z)))]$$ + +Thay đổi, $$z$$ thành biến $$x$$ thuộc distribution của data được sinh $$P_g(x)$$. + +$$L(D, G) = \mathbb{E}_{x \sim P_r(x)}[log(D(x))] + \mathbb{E}_{x \sim P_g(x)}[log(1 - D(x))]$$ + + +Tìm giá trị tối ưu của discriminator, vì khi đó discriminator đã không còn thể được tối ưu nữa và chúng ta chỉ cần tập trung vào generator. + +$$L(D, G) = \int_{x}(P_r(x)log(D(x)) + P_g(x)log(1 - D(x)))dx$$ + +Thay thế $$\tilde{x} = D(x), A = P_r(x), B = P_g(x)$$ vào phương trình trên, ta được cách biểu diễn mới như sau: + +$$\rightarrow f(\tilde{x}) = Alog(\tilde{x}) + Blog(1 - \tilde{x})$$ + +Tìm $$\tilde{x}$$ sao cho phương trình trên đạt điểm cực tiểu, tức **discriminator đã đạt điểm tối ưu**. + +$$\frac{\partial f(\tilde{x})}{\partial \tilde{x}} = 0 \leftrightarrow \frac{A}{ln(10)\tilde{x}} - \frac{B}{ln(10)(1-\tilde{x})} = 0$$ + +$$\leftrightarrow \tilde{x} = \frac{A}{A+B} =\frac{P_r(x)}{P_r(x) + P_g(x)} \in [0, 1]$$ + + +Khi dữ liệu sinh gần với dữ liệu gốc, $$P_g = P_r$$, $$D^*(x)$$ bằng $$\frac{1}{2}$$. Tức nếu nhìn vào một dữ liệu sinh và dữ liệu gốc, discriminator không nhận biết được đâu là thật và đâu là giả. + +Ta có định nghĩa của khoảng cách Jensen-Shannon như sau: + +$$D_{JS}(P_r||P_g) = \frac{1}{2}(D_{KL}(P_r || \frac{P_r + P_g}{2}) + D_{KL}(P_g || \frac{P_r + P_g}{2}))$$ + +$$\leftrightarrow \frac{1}{2}(log(2) + \int_x{P_r(x)log(\frac{P_r(x)}{P_r(x) + P_g(x)})dx} + log(2) + \int_x{P_g(x)log(\frac{P_g(x)}{P_r(x) + P_g(x)})dx})$$ + +$$\leftrightarrow D_{JS}(P_r||P_g) = \frac{1}{2}(log(4) + L(G, D^*))$$ + +$$\rightarrow L(G, D^*) = 2D_{JS}(P_r||P_g) - 2log(2)$$ + +Với biểu diễn trên, ta thấy việc tối ưu generator (discriminator đã đạt điểm tối ưu) cũng là đang tối ưu khoảng cách Jensen-Shannon. Vì vậy, ta có thể kết luận hàm loss của GANs truyền thống là khoảng cách Jensen-Shannon giữa 2 phân phối. + +#### 2.3. Tại sao khoảng cách Wasserstein lại tốt hơn khoảng cách Kullback-Leibler, Jensen-Shannon ? + +
+ +
+ +Giả sử ở trường hợp trên, distribution P nằm ở 0.0 và Q nằm ở 0.5. Với từng cách tính khoảng cách, chúng ta sẽ có được kết quả khác nhau. Cụ thể như sau: + +Với khoảng cách Kullback-Leibler, + +$$D_{KL}(P||Q) = \sum_{i=0}^{n}P(x_i)log(\frac{P(x_i)}{Q(x_i)}) = 1 \times log(\frac{1}{0}) + 0 \times log(\frac{0}{1}) = +\infty$$ + +$$D_{KL}(Q||P) = \sum_{i=0}^{n}Q(x_i)log(\frac{Q(x_i)}{P(x_i)}) = 0 \times log(\frac{0}{1}) + 1 \times log(\frac{1}{0}) = +\infty$$ + +Như các bạn thấy, khoảng cách Kullback-Leibler cho giá trị vô cùng và việc này sẽ dẫn tới hiện tượng exploding gradient. + +Với khoảng cách Jensen-Shannon, + +$$D_{JS}(P||Q) = \frac{1}{2}(D_{KL}(P||\frac{P+Q}{2}) + D_{KL}(Q||\frac{P+Q}{2}))$$ + +$$D_{JS}(P||Q) = \frac{1}{2}(\sum_{i=0}^{n}P(x_i)log(\frac{2P(x_i)}{P(x_i) + Q(x_i)})) + \sum_{i=0}^{n}Q(x_i)log(\frac{2Q(x_i)}{P(x_i) + Q(x_i)})$$ + +$$D_{JS}(P||Q) = \frac{1}{2}(1 \times log(\frac{2}{1}) + 0 \times log(\frac{0}{1}) + 0 \times log(\frac{0}{1}) + 1 \times log(\frac{2}{1})) = log(2)$$ + +Với cách tính của khoảng cách Jensen, dù cho distribution Q có di chuyển gần vào P hoặc xa ra P thì khoảng cách vẫn là $$log(2)$$ và vì thế gradient sẽ không có sự thay đổi. + +Với khoảng cách Wasserstein, nhược điểm trên được khắc phục + +$$D_{Wasserstein}(P||Q) = |\theta| = d$$ + +Với khoảng cách Wasserstein, metric này biểu diễn rất chính xác khoảng cách của 2 distribution dù chúng nằm xa hay gần. Và dựa vào đó, chúng ta thấy sự vượt trội của khoảng cách Wasserstein so với 2 phương pháp đo khoảng cách kia. + +### 3. Code WGAN with Pytorch + +Về cơ bản, cách xây dựng generator và discriminator của WGAN giống với GAN truyền thống. + +* **Step 1: Import các thư viện cần thiết** + +```python +import torch +from torch import nn, optim +import numpy as np +import matplotlib.pyplot as plt +from torch.utils.data import DataLoader, Dataset +from torchvision.datasets import MNIST +import torchvision +import torch.nn.functional as F +from tqdm import tqdm +import torchvision.utils as vutils + +%matplotlib inline +``` + +* **Step 2: Load dataset** + +```python +transform = torchvision.transforms.Compose([ + torchvision.transforms.ToTensor() + ]) + +BATCH_SIZE = 64 +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +train_loader = DataLoader(MNIST("./data/", train=True, download=True, transform=transform), batch_size = BATCH_SIZE, shuffle=True) +``` + +* **Step 3: Xây dựng mô hình Generator và Discriminator** + +Đầu tiên, ta sẽ xây dựng khối convolution gồm Conv + Batch Norm + ReLU +```python +class Conv(nn.Module): + def __init__(self, in_channels: int, out_channels: int, + kernel_size: int = 3, stride: int = 1, padding: int =1): + super().__init__() + self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False) + self.bn = nn.BatchNorm2d(out_channels) + self.act = nn.ReLU() + + self.block = nn.Sequential(self.conv, self.bn, self.act) + + def forward(self, x): + return self.block(x) +``` + +Sau khi có khối Conv, ta dùng nó để xây dựng discriminator và generator + +```python +class Discriminator(nn.Module): + def __init__(self): + super().__init__() + self.conv1 = Conv(1, 16, 3, 1, 1) + self.conv2 = Conv(16, 32, 3, 1, 1) + self.conv3 = Conv(32, 64, 3, 1, 1) + self.pooling = nn.MaxPool2d(2, 2) + + self.fc1 = nn.Linear(7*7*64, 1) + + + def forward(self, x: torch.Tensor): + x = self.conv1(x) + x = self.pooling(x) + + x = self.conv2(x) + x = self.pooling(x) + + x = self.conv3(x) + + x = x.flatten(1) + + x = self.fc1(x) + return x +``` + +```python +class Generator(nn.Module): + def __init__(self, z_dim: int = 20): + super().__init__() + self.fc1 = nn.Linear(z_dim, 7*7*256) + + self.conv1 = Conv(256, 128, 3, 1, 1) + self.conv2 = Conv(128, 64, 3, 1, 1) + self.conv3 = nn.Conv2d(64, 1, 1, 1, bias=True) + self.upsample = nn.UpsamplingBilinear2d(scale_factor=2) + + def forward(self, x: torch.Tensor): + x = self.fc1(x) + x = x.reshape(-1, 256, 7, 7) + + x = self.upsample(x) + x = self.conv1(x) + + x = self.upsample(x) + x = self.conv2(x) + x = self.conv3(x) + x = nn.Sigmoid()(x) + + return x +``` + +* **Step 4: Thiết lập các optimizers, hàm train, visualise** + +Set up các thông số training +```python +GEN_LR = 5e-5 +DISC_LR = 5e-5 +n_critic = 5 + +gen_optimizer = optim.Adam(generator.parameters(), lr=GEN_LR, amsgrad=True) +disc_optimizer = optim.Adam(disc.parameters(), lr=DISC_LR, amsgrad=True) +``` + +Hàm train 1 epoch + +```python +def train_1_epoch(loader, n_critic: int = 5, c: float = 0.01): + wasserstein_distance_1_epoch = 0 + for real_data, _ in tqdm(loader): + real_data = real_data.to(DEVICE) + z = torch.randn((BATCH_SIZE, Z_DIM)).to(DEVICE) + generated_data = generator(z) + for _ in range(n_critic): + disc_optimizer.zero_grad() + wasserstein_distance = -(torch.mean(disc(real_data)) - torch.mean(disc(generated_data))) + + wasserstein_distance.backward(retain_graph=True) + disc_optimizer.step() + + for p in disc.parameters(): + p.data.clamp_(-c, c) + wasserstein_distance_1_epoch -= wasserstein_distance.item() + gen_loss = -torch.mean(disc(generated_data)) + gen_optimizer.zero_grad() + gen_loss.backward() + gen_optimizer.step() + + return wasserstein_distance_1_epoch +``` + +Hàm visualise + +```python +@torch.no_grad() +def visualise(): + generator.eval() + z = torch.randn((16, Z_DIM)).to(DEVICE) + generated_data = generator(z).cpu() + + fig, ax = plt.subplots(figsize=(10, 10)) + plt.axis("off") + ax.imshow(np.transpose(vutils.make_grid(generated_data, padding=2, normalize=True), (1, 2, 0))) + plt.show() + generator.train() +``` + +```python +def train(epochs: int): + for i in range(1, epochs+1): + wasserstein_distance_1_epoch = train_1_epoch(train_loader) + print(f"Epoch: {i} Wasserstein distance of an epoch: {wasserstein_distance_1_epoch}") + visualise() +``` + +* **Step 5: Training model** + +```python +train(1000) +``` + + +### 4. Giải thích đối ngẫu Kantorovich-Rubinstein + +Sự khác biệt giúp WGAN vượt trội hơn GAN truyền thống đó là hàm loss. Hàm loss này giúp khắc phục điểm yếu của các phương pháp cũ và là nhân tố giúp WGAN trở thành SOTA ở thời điểm ra mắt. Tuy nhiên, hàm loss này có quá trình chứng minh và biến đổi tuy phức tạp nhưng rất thú vị. Vì vậy, chúng ta cùng tìm hiểu về đối ngẫu Kantorovich-Rubinstein, nền tảng cho Wasserstein loss nhé. + +Ở trong paper, tác giả đưa ra kết quả cuối để tính Wasserstein distance như sau: + +$$W(\mathbb{P_r}, \mathbb{P_\theta}) = \sup_{||f||_L \le 1} \mathbb{E_{x \sim \mathbb{P_r}}}[f(x)] - \mathbb{E_{x \sim \mathbb{P_\theta}}}[g(x)]$$ + +Để có thể có được kết quả cuối như trên, tác giả đã vận dụng đối ngẫu Kantorovich-Rubinstein. Đối ngẫu này được diễn giải như sau: + +$$W(\mathbb{P_r}, \mathbb{P_g}) = \inf_{\gamma \in \Pi (\mathbb{P_r}, \mathbb{P_g})}{\mathbb{E}_{(x, y) \sim \gamma}[||x-y||]}$$ + +Với 2 marginal constraints: + +$$\mathbb{P}_r(x) = \int_{\chi} d\gamma(x, y)dy$$ + +$$\mathbb{P}_g(y) = \int_{\chi} d\gamma(x, y)dx$$ + +Áp dụng Larange multipliers, ta được hàm tối ưu như sau: + +$$L(\pi, f, g) = \int_{\chi \times \chi}||x-y|| \pi(x, y)dxdy + \int_{\chi}(\mathbb{P}_r(x) - \int_{\chi}\pi(x, y)dy)f(x)dx + \int_{\chi}(\mathbb{P}_g(y) - \int_{\chi}\pi(x, y)dx)g(y)dy$$ + +$$L(\pi, f, g) = \mathbb{E_{x \sim \mathbb{P}_r(x)}}[f(x)] + \mathbb{E_{y \sim \mathbb{P}_g(y)}}[g(y)] + \int_{\chi \times \chi}(||x-y|| - f(x) - g(y)) \pi(x, y)dxdy$$ + +Ta có khoảng cách Wasserstein được định nghĩa như sau: + +$$W(\mathbb{P_r}, \mathbb{P_\theta}) = \inf_{\pi} \sup_{f, g} L(\pi, f, g)$$ + +Vì khoảng cách Wasserstein là hàm lồi convex và chắc chắn có nghiệm nên nó thỏa đối ngẫu mạnh (strong duality). + +Biến đổi đối ngẫu, ta được: + +$$W(\mathbb{P_r}, \mathbb{P_\theta}) = \inf_{\pi} \sup_{f, g} L(\pi, f, g) = \sup_{f, g} \inf_{\pi} L(\pi, f, g)$$ + +$$W(\mathbb{P_r}, \mathbb{P_\theta}) = \sup_{f, g} \inf_{\pi} \mathbb{E_{x \sim \mathbb{P}_r(x)}}[f(x)] + \mathbb{E_{y \sim \mathbb{P}_g(y)}}[g(y)] + \int_{\chi \times \chi}(||x-y|| - f(x) - g(y)) \pi(x, y)dxdy$$ + +Ở phương trình trên, ta có 2 expections độc lập với $$\pi$$ nên có thể rút ra ngoài. + +$$W(\mathbb{P_r}, \mathbb{P_\theta}) = \sup_{f, g} \mathbb{E_{x \sim \mathbb{P}_r(x)}}[f(x)] + \mathbb{E_{y \sim \mathbb{P}_g(y)}}[g(y)] + \inf_{\pi} \int_{\chi \times \chi}(||x-y|| - f(x) - g(y)) \pi(x, y)dxdy$$ + +Đối với vế sau của phương trình, + +$$\inf_{\pi} \int_{\chi \times \chi}(||x-y|| - f(x) - g(y)) \pi(x, y)dxdy = 0 \quad \text{if } ||x-y|| \ge f(x) + g(y)$$ + +Áp dụng điều kiện trên, ta được: + +$$ +W(p, p_g) = \sup_{\substack{f, g \\ f(x) + g(y) \leq ||x - y||}} L(\pi, f, g) = \sup_{\substack{f, g \\ f(x) + g(y) \leq ||x - y||}} \mathbb{E_{x \sim \mathbb{P}_r(x)}}[f(x)] + \mathbb{E_{y \sim \mathbb{P}_g(y)}}[g(y)] +$$ + + +Tới đây, chúng ta đã có thể thay hàm neural networks vào $$f$$ và $$g$$ hoặc cho chúng cùng params để có thể tính được khoảng cách Wasserstein. Tuy nhiên, có một rào cản khiến cho việc này khó khả thi. + +Giả sử, mình sẽ đặt $$g = f$$ và được parameterised $$g_\phi = f_\phi$$. Biến đổi phương trình trên, ta được: + +$$ +W(p, p_g) = \sup_{\substack{f, g \\ f_\phi(x) + f_\phi(y) \leq ||x - y||}} \mathbb{E_{x \sim \mathbb{P}_r(x)}}[f_\phi(x)] + \mathbb{E_{y \sim \mathbb{P}_g(y)}}[f_\phi(y)] +$$ + + +Rào cản khiến cho việc biến đổi đến đây vẫn chưa thể tính được khoảng cách Wasserstein là ràng buộc +$$f_\phi(x) + f_\phi(y) \leq ||x - y||$$ +. Ở thời điểm hiện tại, việc implement một mạng neural network để có thể giống như ràng buộc trên là rất khó. Vì vậy, chúng ta sẽ tiếp tục biến đổi với mong muốn có được một dạng ràng buộc nào đó có thể dễ xấp xỉ bằng mạng neural network hơn. + + +Định lý Kantorovich-Rubinstein đề xuất "_infimal convolution_", nó được định nghĩa như sau: + +$$\kappa(x) = \inf_{u}\{||x-u|| - g(u)\}$$ + +Với việc định nghĩa hàm $$\kappa(x)$$, định lý này chứng minh đây là hàm 1-Lipschitz như sau: + +$$\kappa(x) \leq ||x-u|| - g(u) \leq ||x-y|| + ||y-u|| - g(u) \quad \text{(Định lý hình tam giác)}$$ + +$$\Leftrightarrow \kappa(x) \leq ||x-y|| + \inf_{u}\{||y-u|| - g(u)\} \leq ||x-y|| + \kappa(y)$$ + +$$\Leftrightarrow \kappa(x) - \kappa(y) \leq ||x-y||$$ + +Đổi lại thứ tự biến $$x$$ và $$y$$, ta được + +$$\Leftrightarrow \kappa(y) - \kappa(x) \leq ||x-y||$$ + +Vì vậy ta có thể kết luận hàm $$\kappa$$ là hàm 1-Lipschitz. + +Bây giờ chúng ta sẽ biến đổi ràng buộc +$$f(x) + g(y) \leq ||x - y||$$ + sang một biểu thức đơn giản để tính hơn như sau: + +Ta có: + +$$f(x) + g(y) \leq ||x - y||$$ + +$$\Leftrightarrow f(x) \leq ||x - y|| - g(y)$$ + +$$\Leftrightarrow f(x) = \inf_{y}\{||x - y|| - g(y)\} = \kappa(x)$$ + +$$\kappa(y) \leq ||y - y|| - g(y) = -g(y)$$ + +$$\rightarrow g(y) \leq -\kappa(y)$$ + +Áp dụng 2 phương trình trên vào, ta được: + +$$ +\mathbb{E_{x \sim p}}[f(x)] + \mathbb{E_{y \sim q}}[g(y)] \leq \mathbb{E_{x \sim \mathbb{P}_r(x)}}[\kappa(x)] - \mathbb{E_{y \sim \mathbb{P}_g(y)}}[\kappa(y)] +$$ + +Tổng kết lại, chúng ta sẽ có được phương trình tối ưu cuối cùng như sau: + +$$ +W(p, p_g) \leq \sup_{\substack{\kappa \\ ||\kappa||_L \leq 1}} \mathbb{E_{x \sim \mathbb{P}_r(x)}}[\kappa(x)] - \mathbb{E_{y \sim \mathbb{P}_g(y)}}[\kappa(y)] +$$ + +Và đây là hàm chúng ta dùng để tối ưu model, vì chúng ta chỉ quan tâm tới việc tối thiểu hàm loss nên sẽ không thật sự cần tính đúng khoảng cách Wasserstein, chỉ cần tìm được cận trên hoặc cận dưới là đủ. + + +### 5. Điểm yếu còn tồn đọng của WGAN + +Như các bạn thấy ở trên, tác giải sử dụng phương pháp weight clipping để làm cho mạng discriminator behave giống hàm 1-Lipschitz nhất có thể. Tuy nhiên cách này không đúng và phụ thuộc khá nhiều vào hyperparameter _c_, tác giả cũng thừa nhận trong paper "_Weight clipping is a clearly terrible way to enforce a Lipschitz constraint_". Nếu muốn hệ số c lớn hơn, ta phải đánh đổi bằng việc sử dụng mạng discriminator lớn hơn và hội tụ chậm hơn, ngược lại, nếu c nhỏ hơn thì hội tụ sẽ nhanh hơn nhưng sẽ bị vanishing gradient. + +### 6. Kết luận + +Mô hình WGAN đã gây ra tiếng vang rất lớn khi vừa được ra mắt. Tưởng chừng như đã thay đổi cuộc cách mạng GANs-based generative AI nhưng dường như nó vẫn bị kìm hàm bởi các điểm yếu được liệt kê ở trên. Tuy mặc dù đa số mô hình GANs hiện tại vẫn dùng hàm loss truyền thống, cách thức tiếp cận vấn đề của WGAN vẫn là rất tiềm năng và đáng học hỏi. + +Mình đã giới thiệu về WGAN, cách hoạt động, cũng như code from scratch mô hình này. Nếu có thắc mắc, hãy gửi về email cá nhân của mình và mình sẽ giải đáp. Chúc các bạn học tốt, peace. + +### References +1\. [Wasserstein GAN - arXiv][paper] +2\. [Wasserstein GAN and the Kantorovich-Rubinstein Duality][vincent_blog] +3\. [Kantorovich-Rubinstein Duality ][kr_duality] +4\. [Introduction to the Wasserstein distance - Youtube][wasserstein_metric] +5\. [From GAN to WGAN][lil_blog] + + +[paper]: https://pabloinsente.github.io/the-convolutional-network +[vincent_blog]: https://www.youtube.com/watch?v=05VABNfa1ds +[kr_duality]: https://shuzhanfan.github.io/2018/05/understanding-mathematics-behind-support-vector-machines/ +[wasserstein_metric]: https://www.youtube.com/watch?v=CDiol4LG2Ao&t=301s +[lil_blog]: https://lilianweng.github.io/posts/2017-08-20-gan/ \ No newline at end of file diff --git a/_posts/2024-05-23-vision_transformer.md b/_posts/2024-05-23-vision_transformer.md new file mode 100644 index 00000000000..3d6012a365e --- /dev/null +++ b/_posts/2024-05-23-vision_transformer.md @@ -0,0 +1,250 @@ +--- +title: "Giải thích và code Vision Transformer from scratch" +mathjax: true +layout: post +categories: media +--- + + +Xin chào các bạn, + + +Trong bài post này, mình sẽ giới thiệu chi tiết và code from scratch kiến trúc Vision Transformer. + +
+ +
+ + +### 1. Giới thiệu + + +Sau khi transformer được ra mắt vào năm 2014, nó đã tạo ra một sự đột phá và cách mạng trong lĩnh vực NLP (Natural Language Processing) với tốc độ tính toán và performance cao hơn so với kiến trúc RNN-based trước đó. Ở thời điểm mà transformer ra mắt, mọi người chỉ sử dụng nó cho NLP và không nghĩ nó sẽ được dùng cho ảnh. Alexey Dosovitskiy et al. đã nghiên cứu áp dụng kiến trúc này cho computer vision và publish nó trong paper [_"An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale"_][vision transformer paper]. + + +### 2. Kiến trúc + + +Về tổng quan, mô hình Vision transformer thừa hưởng 99% kiến trúc của transformer nguyên bản. Sự khác biệt đến từ việc biến đổi ảnh để nó giống với format text khi ta đưa vào transformer truyền thống. + +Alex đã giải quyết vấn đề này bằng cách tách tấm ảnh thành các vùng nhỏ bằng nhau (patches) về kích thước và ghép nó lại thành một chuỗi. Sau bước biến đổi này thì tấm hình của chúng ta đã giống với format của text và việc xử lý các khâu còn lại gần như tương tự như transformer. + +Input của mô hình transformer: + +$$\R^{batch\_size \times \text{sequence\_length} \times \text{embedding\_dimension}}$$ + +Phép biến đổi input của vision transformer: + +$$\R^{Height \times Width \times Channels} => \R^{\text{num\_patches} \times (\text{patch\_size} \times \text{patch\_size} \times \text{channels})}$$ + + + +![Image](https://production-media.paperswithcode.com/methods/Screen_Shot_2021-01-26_at_9.43.31_PM_uI4jjMq.png) + + +### 3. Code It Up + +Phần code của Vision Transformer gồm 3 phần chính: Patchifier, Embedding Layer, và khối module chính (transformer). + +* **Import các thư viện cần thiết** + +```python +import torch +import torch.nn as nn +import numpy as np +``` + +* **Patchifier** + +```python +class PatchExtractor(nn.Module): + def __init__(self, patch_size = 10): + super().__init__() + self.patch_size = patch_size + + def forward(self, input_data): + batch_size, channels, height, width = input_data.size() + assert height % self.patch_size == 0 and width % self.patch_size == 0, \ + f"Input height ({height}) and width ({width}) are not divisible by patch_size ({self.patch_size})" + + num_patches_h = height//self.patch_size + num_patches_w = width//self.patch_size + num_patches = num_patches_h*num_patches_w + + patches = input_data.unfold(2, self.patch_size, self.patch_size). \ + unfold(3, self.patch_size, self.patch_size). \ + permute(0, 2, 3, 1, 4, 5).contiguous().view(batch_size, num_patches, -1) + + return patches +``` + +* **Embedding Layer** + +```python +class EmbeddingLayer(nn.Module): + def __init__(self, latent_size: int = 1024, + num_patches: int = 4, + input_dim: int = 768): + super().__init__() + + self.num_patches = num_patches + self.pos_embedder = nn.Linear(1, latent_size) + self.input_embedder = nn.Linear(input_dim, latent_size) + self.positional_information = torch.arange(0, self.num_patches).\ + reshape(1, num_patches, 1).float() + + def forward(self, input): + N, num_patches, input_dim = input.shape + input_embedding = self.input_embedder(input) + positional_embedding = torch.tile(self.positional_information, (N, 1, 1)) + positional_embedding = self.pos_embedder(positional_embedding) + return positional_embedding + input_embedding +``` + +Mô hình +```python +class ViT(nn.Module): + def __init__(self, patch_size: int = 16, + img_dimension: tuple = (32, 32), + latent_size: int = 1024, + num_heads: int = 1, + num_classes: int = 2): + super().__init__() + + assert img_dimension[0]%patch_size == 0 and \ + img_dimension[1]%patch_size == 0, "Patch size is not divisible by image dimension !!" + + + self.num_patches_h = img_dimension[0]//patch_size + self.num_patches_w = img_dimension[1]//patch_size + self.num_patches = self.num_patches_h * self.num_patches_w + + self.patchifier = PatchExtractor(patch_size) + self.embedding_layer = EmbeddingLayer(latent_size=latent_size, + num_patches=self.num_patches, + input_dim=patch_size*patch_size*3) + self.multi_head_attn = nn.MultiheadAttention(embed_dim=latent_size, num_heads=num_heads) + self.norm_1 = nn.LayerNorm(normalized_shape=latent_size) + self.norm_2 = nn.LayerNorm(normalized_shape=latent_size) + + self.feed_forward_block = nn.Sequential(nn.Linear(latent_size, latent_size*2), + nn.Linear(latent_size*2, latent_size)) + + self.output_layer = nn.Linear(latent_size*self.num_patches, num_classes) + def forward(self, x): + x = self.patchifier(x) + x = self.embedding_layer(x) + + x = self.norm_1(self.multi_head_attn(x, x, x)[0] + x) + + x = self.norm_2(self.feed_forward_block(x) + x) + + x = self.output_layer(x.flatten(start_dim=1)) + return x +``` + +* **Training (Optional)** + +```python +import torch +import torchvision +import torchvision.transforms as transforms +from tqdm import tqdm +import cv2 +import matplotlib.pyplot as plt +import torch.nn as nn +from torch import optim + +transform = transforms.Compose( + [transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) + +BATCH_SIZE = 16 + +trainset = torchvision.datasets.CIFAR10(root='./data', train=True, + download=True, transform=transform) +trainloader = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE, + shuffle=True, num_workers=2) + +testset = torchvision.datasets.CIFAR10(root='./data', train=False, + download=True, transform=transform) +testloader = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE, + shuffle=False, num_workers=2) + +classes = ('plane', 'car', 'bird', 'cat', + 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') + +model = ViT(patch_size=16, + img_dimension=(32, 32), + latent_size=1024, + num_heads=2, + num_classes=10) + +LR = 1e-3 + +optimizer = optim.Adam(model.parameters(), lr = 1e-3, amsgrad = True) +loss_fn = nn.CrossEntropyLoss() + +def train_1_epoch(train_loader): + loss_value = 0 + cnt = 0 + for (x, y) in tqdm(train_loader): + logits = model(x) + loss = loss_fn(logits, y) + optimizer.zero_grad() + loss.backward() + optimizer.step() + + loss_value += loss.item() + cnt += 1 + + return loss_value/cnt + + +@torch.no_grad() +def eval(testloader): + model.eval() + loss_value = 0 + cnt = 0 + num_correct = 0 + num_samples = 0 + for (x, y) in tqdm(testloader): + logits = model(x) + loss = loss_fn(logits, y) + loss_value += loss.item() + + pred = logits.argmax(1) + num_correct += len(pred[pred==y]) + num_samples += len(y) + cnt += 1 + + model.train() + return loss_value/cnt, num_correct/num_samples + +def train(epochs): + for epoch in range(epochs): + train_loss = train_1_epoch(trainloader) + val_loss, val_acc = eval(testloader) + + + print(f"Epoch: {epoch} Train Loss: {train_loss} Validation Loss: {val_loss} Val Acc: {val_acc}") + + +train(10) +``` + +``` +Add result here +``` + +### 4. Kết + +Ở bài post này, mình đã giới thiệu về kiến trúc Vision Transformer và code nó from scratch một cách đơn giản và dễ hiểu nhất. Ở các bài post tiếp theo, mình sẽ giới thiệu các kiến trúc transformer-based khác cho các task như image segmentation, object detection, ... Chúc các bạn học tốt. + + + +### References +1\. [An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale][vision transformer paper] + + +[vision transformer paper]: google.com \ No newline at end of file diff --git a/_posts/2024-05-25-data_compression.md b/_posts/2024-05-25-data_compression.md new file mode 100644 index 00000000000..76a4df2e4b0 --- /dev/null +++ b/_posts/2024-05-25-data_compression.md @@ -0,0 +1,264 @@ +--- +title: "Giải thích và code các giải thuật data compression - Phần 1" +mathjax: true +layout: post +categories: media +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ giới thiệu về nén data (data compression). + +### 1. Giới thiệu + +Nếu các bạn dùng máy tính, chắc chắn đã từng thấy từ "Compress file" khi right click vào một folder nào đó. Compress file này tức là mã hóa (encode) folder này lại thành một đoạn code có thể được giải mã với ít bits hơn nhằm **giảm dung lượng lưu trữ** của chúng để có thể dễ dàng di chuyển. + +Vì vậy, nén data là lĩnh vực dùng các giải thuật để có thể mã hóa dữ liệu thành một đoạn dữ liệu có dung lượng nhỏ hơn và có thể giải mã để lấy lại dữ liệu gốc khi ta muốn. Data compression được chia thành 2 nhóm chính: **lossy compression** và **lossless compression**. Lossy compression là giải thuật nén ảnh làm mất một phần nào đó của dữ liệu gốc và lossless là giải thuật nén ảnh mà không làm mất dữ liệu gốc. + +* Những khác biệt chính giữa lossy và lossless compression + +| | Lossy | Lossless | +|----|----|---| +|Chất lượng | Mất mát 1 phần | Không mất| +|Applications | Ảnh, videos, nhạc| Ảnh, videos, nhạc, **text**| +|File types | Images: JPEG | Images: RAW, BMP, PNG| +| | Video: MPEG, AVC, HEVC | General: ZIP| +| | Audio: MP3, AAC | Audio: WAV, FLAC| + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 1.1. Lossy vs Lossless compression
+
+ + +### 2. Lossless Compression algorithms + +Ở phần này, mình sẽ giới thiệu 2 giải thuật phổ biến để nén data mà không làm mất mát dữ liệu là Run Length Encoding, Huffman Encoding. + +### 2.1. Run Length Encoding + +Phương pháp này khá đơn giản cả về mặt lý thuyết và thực hiện, tuy tính hiệu quả của nó không quá cao và thậm chí có thể "phản dame". + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 2.1.1. Cách hoạt động của Run Length Encoding
+
+ +Như ở hình trên, các bạn cũng đã có thể mường tượng được cách làm việc của phương pháp này. Diễn giải bằng lời, run length encoding rút gọn dữ liệu bằng cách chèn số lần xuất hiện liên tiếp của 1 element vào vị trí đứng trước hoặc sau nó. + + +Ví dụ, + +Ta có một chuỗi (string) A = "AAAABBBB" và số lượng bit cần lưu trữ là 8 bytes * 8 bits = 64 bits. Sau khi dùng RLE, ta có được kết quả "4A4B" và số lượng bit cần được lưu trữ là 4 bytes * 8 bits = 32 bits. Trong trường hợp này, việc sử dụng RLE đã giúp chúng ta giảm được 1 nửa số lượng dung lượng cần cấp cho file này. + +Tuy nhiên trong trường hợp, data đứng tách rời nhau như "ABCDEFG" ta có được kết quả từ RLE "1A1B1C1D1E1F1G" thì số lượng luu trữ sẽ tăng gấp đôi. Vì vậy, việc sử dụng RLE có thể tăng số bits cần lưu trữ. + +* **Code** +```python +class RLE: + def __init__(self) -> None: + pass + + @staticmethod + def encode(msg: str): + + prev_char = msg[0] + frequency_list, element_list = [], [] + + cnt = 1 + for i in range(1, len(msg)): + if msg[i] == prev_char: + cnt += 1 + + elif msg[i] != prev_char: + frequency_list.append(cnt) + element_list.append(prev_char) + prev_char = msg[i] + cnt = 1 + + + frequency_list.append(cnt) + element_list.append(prev_char) + + + return frequency_list, element_list + + @staticmethod + def decode(frequency_list: list, + element_list: list): + decoded_msg = "" + assert len(frequency_list) == len(element_list), \ + "Length of frequency list != Length of element list. Please retry!" + for i in range(len(frequency_list)): + decoded_msg += frequency_list[i]*element_list[i] + return decoded_msg +``` + +```python + +string1 = "AAAA1122BBB" +string2 = "ABCDEFG" + +encoded_string1 = RLE.encode(string1) +encoded_string2 = RLE.encode(string2) + +decoded_string1 = RLE.decode(*encoded_string1) +decoded_string2 = RLE.decode(*encoded_string2) + +print(f"Original string 1: {string1}, Encoded string 1: {encoded_string1}, Decoded string 1: {decoded_string1}") +print(f"Original string 2: {string2}, Encoded string 2: {encoded_string2}, Decoded string 2: {decoded_string2}") +``` + + + +```bash +Original string 1: AAAA1122BBB, Encoded string 1: ([4, 2, 2, 3], ['A', '1', '2', 'B']), Decoded string 1: AAAA1122BBB +Original string 2: ABCDEFG, Encoded string 2: ([1, 1, 1, 1, 1, 1, 1], ['A', 'B', 'C', 'D', 'E', 'F', 'G']), Decoded string 2: ABCDEFG +``` + +### 2.2. Huffman Encoding + +Phương pháp Huffman Encoding dùng công thức thống kê (statistics) phức tạp hơn để encode data. Idea chính của giải thuật này là dùng số lượng bit ít để gán cho element có tần suất xuất hiện nhiều và ngược lại. + +Ở phương pháp Run Length Encoding, giải thuật này "sợ" các trường hợp đứng tách rời nhau nhưng đối với Huffman, việc này đã được giải quyết. Vì vậy, dung lượng data được encode bởi Huffman sẽ luôn luôn nhỏ hơn hoặc bằng dung lượng ta cần cấp để lưu file gốc. + +* Ví dụ minh họa tính tay của Huffman encoding + +Giả sử chúng ta có một chuỗi 15 kí tự như sau cần được nén lại: + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 2.2.1. Initial String
+
+ +Ban đầu, chuỗi này sẽ chiếm tổng cộng 15*8 = 120 bits cần được lưu trữ. + +Bước 1: Tính toán tần suất của các ký tự + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 2.2.1. Tần suất lần lượt của các ký tự B C A D
+
+ +Bước 2: Lọc chúng với giá trị từ thấp tới cao + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 2.2.1. Giá trị tần suất từ thấp tới cao
+
+ +Bước 3: Tạo leaf node và gán giá trị của character vào node đó với thứ tự trái nhỏ hơn phải. Ngoài ra, tạo node parent bằng tổng 2 node của nhánh đó. + +
+ The Difference Architecture between AlexNet and VGG16 Models +
Hình 2.2.1. Giá trị tần suất từ thấp tới cao
+
+ +Với cách encoding như vậy, ta có bảng so sánh sau + +|Character|Frequency|Code|Size| +|-|-|-|-| +|A|5|11|5*2 bits| +|B|1|100|1 * 3 bits| +|C|6|0|6 * 1 bits| +|D|3|101|3 * 3 bits| +||15*8=120 bits| |28 bits| + +Vì vậy ở trong bảng trên, cách mã hóa Huffman giúp ta tiết kiệm ~3 lần số bits cần lưu trữ. + +* Code Huffman Encoding + +```python +string = 'BCAADDDCCACACAC' + +# Creating tree nodes +class NodeTree(object): + + def __init__(self, left=None, right=None): + self.left = left + self.right = right + + def children(self): + return (self.left, self.right) + + def nodes(self): + return (self.left, self.right) + + def __str__(self): + return '%s_%s' % (self.left, self.right) + + +# Main function implementing huffman coding +def huffman_code_tree(node, left=True, binString=''): + if type(node) is str: + return {node: binString} + (l, r) = node.children() + d = dict() + d.update(huffman_code_tree(l, True, binString + '0')) + d.update(huffman_code_tree(r, False, binString + '1')) + return d + + +# Calculating frequency +freq = {} +for c in string: + if c in freq: + freq[c] += 1 + else: + freq[c] = 1 + +freq = sorted(freq.items(), key=lambda x: x[1], reverse=True) + +nodes = freq + +while len(nodes) > 1: + (key1, c1) = nodes[-1] + (key2, c2) = nodes[-2] + nodes = nodes[:-2] + node = NodeTree(key1, key2) + nodes.append((node, c1 + c2)) + + nodes = sorted(nodes, key=lambda x: x[1], reverse=True) + +huffmanCode = huffman_code_tree(nodes[0][0]) + +print(' Char | Huffman code ') +print('----------------------') +for (char, frequency) in freq: + print(' %-4r |%12s' % (char, huffmanCode[char])) +``` + +```bash + Char | Huffman code +---------------------- + 'C' | 0 + 'A' | 11 + 'D' | 101 + 'B' | 100 +``` + + + + +### 3. Lời kết + +Trong bài viết này, mình đã trình bày về hai phương pháp nén dữ liệu phổ biến là Run Length Encoding (RLE) và Huffman Encoding. Mỗi phương pháp đều có những ưu và nhược điểm riêng, thích hợp cho các loại dữ liệu khác nhau và tình huống sử dụng khác nhau. RLE đơn giản và hiệu quả đối với các chuỗi dữ liệu có nhiều phần tử lặp lại liên tiếp, trong khi Huffman Encoding mạnh mẽ hơn và có thể tối ưu hóa kích thước lưu trữ cho mọi loại dữ liệu thông qua việc phân bổ số lượng bit dựa trên tần suất xuất hiện. + +Qua phần giới thiệu lý thuyết và các đoạn mã mẫu, hy vọng các bạn đã hiểu rõ hơn về cách hoạt động và ứng dụng của hai phương pháp nén này. Nén dữ liệu không chỉ giúp tiết kiệm dung lượng lưu trữ mà còn tối ưu hóa việc truyền tải thông tin, đặc biệt quan trọng trong thời đại mà dữ liệu và thông tin trở thành nguồn tài nguyên quý giá. + +Nếu có thắc mắc hoặc cần giải đáp thêm về các thuật toán nén khác, các bạn hãy để lại comment hoặc liên hệ trực tiếp với mình. Cảm ơn các bạn đã theo dõi và hy vọng bài viết này sẽ giúp ích cho các bạn trong việc hiểu và áp dụng các phương pháp nén dữ liệu trong công việc và học tập. + + + +### References + +1\. [Huffman Encoding - Programiz][Huffman Encoding - Programiz] + +2\. [Run-length encoding (lossless data compression) - Inside code][Run-length encoding (lossless data compression) - Inside code] + +2\. [ChatGPT - OpenAI][ChatGPT - OpenAI] + + +[Huffman Encoding - Programiz]: google.com +[ChatGPT - OpenAI]: chatgpt.com +[Run-length encoding (lossless data compression) - Inside code]: https://www.youtube.com/watch?v=ix8fnWK7LH8 \ No newline at end of file diff --git a/_posts/2024-06-30-pca.md b/_posts/2024-06-30-pca.md new file mode 100644 index 00000000000..530f171b05d --- /dev/null +++ b/_posts/2024-06-30-pca.md @@ -0,0 +1,177 @@ +--- +title: "Giải thích tường tận và code giải thuật Principal Component Analysis" +mathjax: true +layout: post +categories: media +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ trình bày về một giải thuật dùng để giảm chiều dữ liệu có tên là **Principle Component Analysis (PCA)**. Đây là một giải thuật khá cổ tuy nhiên những giá trị toán học nó mang lại vẫn trường tồn theo thời gian. Chúng ta hãy cùng khám phá xem nó hoạt động như thế nào nhé. + +![Image](https://kindsonthegenius.com/blog/wp-content/uploads/2018/11/Principal-2BComponents-2BAnalysis-2BTutorial.jpg) + +### 1. Giới thiệu + +Như đã đề cập ở tiêu đề, PCA là một giải thuật dùng để giảm chiều của dữ liệu trong khi tối thiểu thông tin mất mát. Nó có rất nhiều ứng dụng thực tế như: + +* **Visualise data** +* **Data Compression** +* + + +### 2. How it works + +Ở phần này, mình sẽ hướng dẫn implement PCA với chỉ thư viện numpy. Bộ dữ liệu minh sẽ sử dụng là tập Iris dataset từ thư viện sklearn. + + +* **Bước 1**: Load các thư viện cần sử dụng +```python +import numpy as np # For calculation +from sklearn import datasets # For dataset +import matplotlib.pyplot as plt # For visualisation + +%matplotlib inline +``` + +```python +data = datasets.load_iris()["data"] +print(f"Data dimension: {data.shape}") +``` + +```bash +Data dimension: (150, 4) +``` + +Tập Iris dataset có dimension là 4, chúng ta muốn giảm nó xuống 2 hoặc 3 để có thể visualise nó. Và chúng ta sẽ thực hiện visualise bằng PCA. + +* **Bước 2**: Normalise data + +Trước khi áp dụng PCA, data phải cần được tiêu chuẩn hóa cho các feature. Điều này giúp cho tất cả các feature của ma trận covariance có cùng 1 range với nhau. + +```python +data = data - data.mean(axis=0, keepdims=True) +``` + +* **Bước 2**: Tìm eigen values và eigen vectors + +Ở bước 2 này, chúng ta sẽ tìm tất cả eigen values và eigen vectors của ma trận covariance data. + +```python +data_cov = np.cov(data.T) +eigen_values, eigen_vectors = np.linalg.eig(data_cov) +eigen_vectors = eigen_vectors.T +``` + +* **Bước 3**: Lọc ra k eigenvectors có eigenvalues lớn nhất + +```python + +pca_dim = 2 +assert pca_dim < data.shape[-1], "pca dim > num feature, invalid operation" +idxes = np.argsort(eigen_values)[::-1] +chosen_eigen_vectors = eigen_vectors[idxes[:pca_dim]].T +``` + +* **Bước 4**: Dùng **k** eigenvectors để làm principal components + +```python +new_data = data.dot(chosen_eigen_vectors) +print(f"Old data shape: {data.shape}, New data shape: {new_data.shape}") +``` + +```bash +Old data shape: (150, 4), New data shape: (150, 2) +``` + +Và đây là class được tích hợp từ các steps trên để thực hiện PCA cho dữ liệu. + +```python +def pca(data: np.ndarray, + pca_dim: int): + + # Step 1: Rescale data + data = data - data.mean(axis=0) + + data_cov = np.cov(data.T) + eigen_values, eigen_vectors = np.linalg.eig(data_cov) + eigen_vectors = eigen_vectors.T + + idxes = np.argsort(eigen_values)[::-1] + chosen_eigen_vectors = eigen_vectors[idxes][:pca_dim].T + new_data = data.dot(chosen_eigen_vectors) + print(f"Old data shape: {data.shape}, New data shape: {new_data.shape}") + return new_data +``` + +* **Test** + +Đối với giảm chiều dữ liệu, chúng ta cũng có thể cắt bỏ các chiều dữ liệu khác một cách ngẫu nhiên, tuy nhiên PCA cho chúng ta một dữ liệu với ít chiều hơn nhưng lượng thông tin giữ lại được có nhiều **ý nghĩa** hơn so với việc bỏ những dimension khác một cách ngẫu nhiên. Và trong nhiều trường hợp, bạn sẽ thấy việc dùng PCA còn giúp tăng đáng kể performance của ML models vì PCA giúp loại ra được những chiều thông tin dư. + +Ở đây, mình sẽ thực nghiệm với tập dataset iris của sklearn để xem performance của machine learning khi được train trên PCA. + +```python +# Step 1: Import necessary libraries + +from sklearn import datasets +from sklearn import tree +``` + +```python +# Divide into train and val dataset +iris = datasets.load_iris()["data"] +X = datasets.load_iris().data +Y = datasets.load_iris().target + +idxes = np.random.permutation(len(X)) + +X_train = X[idxes[:100]] +Y_train = Y[idxes[:100]] + +X_val = X[idxes[100:]] +Y_val = Y[idxes[100:]] +print(f"X_train shape: {X_train.shape}, Y_train shape: {Y_train.shape}, X_val shape: {X_val.shape}, Y_val shape: {Y_val.shape}") + +``` + +```python +# Perform PCA on X_train dataset +pca = PCA(2) +pca.fit(X_train) +X_train_transformed = pca.transform(X_train) +X_val_transformed = pca.transform(X_val) +``` + +```python +# Init classifiers + +classifier1 = tree.DecisionTreeClassifier() +classifier1 = classifier1.fit(X_train, Y_train) + +classifier2 = tree.DecisionTreeClassifier() +classifier2 = classifier2.fit(X_train_transformed, Y_train) +``` + +```python +# Inference on val set +non_pca_predictions = classifier1.predict(X_val) +pca_predictions = classifier2.predict(X_val_transformed) + +print(f"Without PCA Accuracy: {len(non_pca_predictions[non_pca_predictions==Y_val])/len(non_pca_predictions)}") +print(f"With PCA Accuracy: {len(pca_predictions[pca_predictions==Y_val])/len(pca_predictions)}") +``` + +```bash +Without PCA Accuracy: 0.96 +With PCA Accuracy: 1.0 +``` + +Ở ví dụ này, PCA đã giúp chúng ta tăng accuracy lên 100%. Ở case này, PCA giúp chúng ta cô đọng được thông tin nhiều hơn và có thể đã giảm được hiện tượng overfitting trên tập train nên đã dẫn tới kết quả tốt hơn. + + +### 3. Giải thích toán của PCA + +Principal Component Analysis là lời giải cho bài toán optimisation ứng dụng phương pháp Larange. Ta có phát biểu của bài toán như sau: + + + diff --git a/_posts/2024-09-21-svm.md b/_posts/2024-09-21-svm.md new file mode 100644 index 00000000000..0aab321a20e --- /dev/null +++ b/_posts/2024-09-21-svm.md @@ -0,0 +1,323 @@ +--- +title: "Giải thích và code Support Vector Machine" +mathjax: true +layout: post +--- + +Xin chào các bạn, + +Trong bài post này, mình sẽ giải thích về toán học và code Support Vector Machine. + +
+ +
+ +### 1. Giới thiệu + +Support Vector Machine là một giải thuật phân loại có giám sát (supervised learning) được đề xuất bởi nhà khoa học người Liên Xô Vladimir Vapnik và các cộng sự. Đây là một giải thuật cực kì thú vị cho các bạn mới học ML/AI vì cách diễn giải và chứng minh toán học không quá phức tạp nhưng cực kì hiệu quả. + +Ban đầu, giải thuật này chỉ được sử dụng cho việc phân loại 2 classes (binary classification). Tuy nhiên, sau đó cộng đồng AI/ML đã cải biến để có thể dùng nó cho multiple-class classification, regression. + +### 2. Giải thích toán học + +Các lý thuyết về của SVM bao quanh các định lý cơ bản của toán học như vector, matrix, tối ưu Larange. + +Primal form của SVM có dạng như sau: + +$$\min_{w, b} \frac{1}{2}||w|| + C \sum_{1}^{n}{\xi_i}$$ + + +$$\text{s.t. } y_i(w^T\phi(x_i) + b) \ge 1 - \xi_i, \forall i$$ + +$$\xi_i \ge 0, \forall i$$ + + +Tới đây, nếu không vướng phải hàm $$\phi$$ thì ta có thể dùng các thư viện tối ưu để tìm ra được $$w$$ và $$b$$. Hàm $$\phi$$ là một hàm mà sẽ chiếu dữ liệu gốc lên một không gian mới (có thể nhiều chiều hơn hoặc ít chiều hơn), tuy nhiên chúng ta không biết hàm này và sẽ phải chọn thủ công để tìm được hàm tối ưu nhất cho từng task. + +Có một cách mà có thể bỏ qua được việc biết hàm $$\phi$$ là gì, đó là **biến đổi hàm tối ưu này sang dạng dual và dùng phương pháp kernelisation** (mình sẽ nói rõ hơn ở phần áp dụng nó vào). + +Bây giờ, chúng ta sẽ từng bước biến đổi từ bài toán primal sang dual. Ở bài toán primal, ta sẽ áp dụng phương pháp Lagrange và được như sau: + +$$L(w, b, [{\xi_i}], [\alpha_i], [\lambda_i]) = \frac{1}{2}||w|| + \sum_{i=1}^{n}\alpha_i(1 - \xi_i - y_i(w^T\phi(x_i) + b)) + \sum_{i=1}^{n}\lambda_i \xi_i$$ + +Đây là hàm Lagrange, hàm này biến đổi từ một mớ constraints thành một hàm unconstraints. Để tìm được điểm tối ưu, chúng ta phải giải phương trình sau. + +$$L^*(w, b, [{\xi_i}], [\alpha_i], [\lambda_i]) = \inf_{w} \sup_{[\alpha_i], [\lambda_i]}\frac{1}{2}||w|| + \sum_{i=1}^{n}\alpha_i(1 - \xi_i - y_i(w^T\phi(x_i) + b)) + \sum_{i=1}^{n}\lambda_i \xi_i$$ + +Bình thường, để có thể biến đổi từ dạng primal sang dual mà không làm biến đổi kết quả cuối cùng của bài toán, chúng ta phải chứng minh chúng đối ngẫu (strong duality). Tuy nhiên, bài toán này đã được chứng minh là đối ngẫu nếu primal có nghiệm (chắc chắn primal của bài toán này có nghiệm), và mình sẽ chứng minh nó đối ngẫu ở các bài riêng. Tạm thời, chúng ta hãy chấp nhận strong duality và tiếp tục biến đổi. + +Với strong duality, ta có hệ quả như sau: + +$$L^* = \inf_{w, b, [\xi_i]} \sup_{[\alpha_i], [\lambda_i]} = \sup_{[\alpha_i], [\lambda_i]} \inf_{w, b, [\xi_i]}$$ + +Áp dụng hệ quả của strong duality, ta được phương trình sau + +$$L^*(w, b, [{\xi_i}], [\alpha_i], [\lambda_i]) = \sup_{[\alpha_i], [\lambda_i]} \inf_{w, b, [\xi_i]} \frac{1}{2}||w|| + \sum_{i=1}^{n}\alpha_i(1 - \xi_i - y_i(w^T\phi(x_i) + b)) + \sum_{i=1}^{n}\lambda_i \xi_i$$ + +Với phương trình trên, ta sẽ tìm $$w$$ và $$b$$ sao cho hàm Lagrange có giá trị nhỏ nhất. Ta dùng đạo hàm để tìm giá trị cực tiểu. + +$$\frac{\partial L}{\partial w} = 0 <=> w - \sum_{i=1}^{n}\alpha_n y_n \phi(x_n) = 0 <=> w = \sum_{i=1}^{n}\alpha_n y_n \phi(x_n)$$ + +$$\frac{\partial L}{\partial b} = 0 <=> \sum_{i=1}^{n}\alpha_n y_n$$ + +$$\frac{\partial L}{\partial \xi} = 0 <=> C - \bold{\alpha} - \bold{\lambda} = 0$$ + +Áp dụng các phương trình trên vào hàm Lagrange, ta được: + +$$L([\alpha_i], [\lambda_i]) = \sum_{i=1}^{n} \alpha_i + \frac{1}{2} \sum_{i=1}^{m}\sum_{i=1}^{n} \alpha_m \alpha_n y_m y_n \phi^T(x_m)\phi(x_n)$$ + +$$\text{s.t. } \alpha_i, \lambda_i \ge 0$$ + +$$\sum_{i=1}^{n}\alpha_i y_i = 0$$ + +$$C - \alpha - \lambda = 0$$ + +Như quan sát, trong làm Lagrange bây giờ không còn tồn tại $$\lambda$$ nhưng $$\lambda$$ vẫn phải lớn hơn 0 do ràng buộc Lagrange. Ta sẽ dùng nó để biến đổi tiếp như sau: + +$$L([\alpha_i], [\lambda_i]) = \sum_{i=1}^{n} \alpha_i + \frac{1}{2} \sum_{i=1}^{m}\sum_{i=1}^{n} \alpha_m \alpha_n y_m y_n \phi^T(x_m)\phi(x_n)$$ + +$$\text{s.t. } \alpha_i \ge 0$$ + +$$\sum_{i=1}^{n}\alpha_i y_i = 0$$ + +$$C - \alpha \ge 0$$ + +Và cuối cùng, ta cần phải giải phương trình với các constraints như sau để có thể tìm ra nghiệm của bài toán. + +$$L^*([\alpha_i], [\lambda_i]) = \sup_{[\alpha_i]} \sum_{i=1}^{n} \alpha_i + \frac{1}{2} \sum_{i=1}^{m}\sum_{i=1}^{n} \alpha_m \alpha_n y_m y_n \phi^T(x_m)\phi(x_n)$$ + +$$\text{s.t. } 0 \le \alpha_i \le C$$ + +$$\sum_{i=1}^{n}\alpha_i y_i = 0$$ + +Nhìn có vẻ phức tạp, nhưng thực chất nó có thể được biểu diễn như bài toán **quadratic convex optimisation** và có thể được giải dễ dàng. Tuy nhiên, chúng ta vẫn còn bị cấn ở phần hàm $$\phi$$ và mục đích biến đổi nãy giờ chỉ để bypass phần này. Để có thể thực hiện việc này, các nhà khoa học đã đề xuất một phương pháp được gọi là **kernelisation trick**. + +Giả sử, hàm $$\phi \in \mathbb{R}^2 \to \mathbb{R}^3$$ có dạng như sau: + +$$\phi: x \to \phi(x) = \begin{bmatrix} x_1^2 \\ \sqrt{2}x_1x_2 \\ x_2^2 \end{bmatrix} $$ + +$$\phi^T(x_m) \cdot \phi(x_n) = \begin{bmatrix} x_{m1}^2 \\ \sqrt{2}x_{m1}x_{m2} \\ x_{m2}^2 \end{bmatrix}^T \cdot \begin{bmatrix} x_{n1}^2 \\ \sqrt{2}x_{n1}x_{n2} \\ x_{n2}^2 \end{bmatrix}$$ + +$$<=> \phi^T(x_m) \cdot \phi(x_n) = x_{m1}^2x_{n1}^2 + 2x_{m1}x_{m2}x_{n1}x_{n2} + x_{m2}^2x_{n2}^2$$ + +$$<=> \phi^T(x_m) \cdot \phi(x_n) = (x_{m1}x_{n1} + x_{m2}x_{n2})^2 = (x_m^T \cdot x_n)^2$$ + +Như các bạn thấy, sau khi dot 2 vectors lại thì ta được một kết quả là dot của 2 vectors gốc không liên quan gì đến $$\phi$$ mà phụ thuộc vào kết quả của một **cách kết hợp của 2 dữ liệu ở chiều gốc** và người ta đặt tên cho nó là kernelisation. + +Có rất nhiều loại kernel, một vài loại phổ biến là: + +$$\text{Polynomial: } k(x_m, x_n) = (x_m^Tx_n + r)^d$$ + + +$$\text{Linear: } k(x_m, x_n) = x_m^Tx_n$$ + + +$$\text{Gaussian: } k(x_m, x_n) = e^{\frac{||x_m - x_n||^2}{2\sigma^2}}$$ + + +$$\text{Laplace: } k(x_m, x_n) = e^{-\alpha||x_m - x_n||}$$ + +Nhưng mình thấy dùng phổ biến nhất có Gaussian. Việc kernel tốt và phù hợp với bài toán sẽ là quyết định của người sử dụng. + +Áp dụng kernelisation vào phương trình Larange ở trên, ta được: + +$$L^*([\alpha_i], [\lambda_i]) = \sup_{[\alpha_i]} \sum_{i=1}^{n} \alpha_i + \frac{1}{2} \sum_{i=1}^{m}\sum_{i=1}^{n} \alpha_m \alpha_n y_m y_n k(x_m, x_n)$$ + +$$\text{s.t. } 0 \le \alpha_i \le C$$ + +$$\sum_{i=1}^{n}\alpha_i y_i = 0$$ + +Sau khi dùng quadratic programming tìm ra nghiệm tối ưu cho phương trình trên, ta sẽ được $$\alpha^*$$ và sẽ dùng nó để tìm $$w$$ và $$b$$. + +Áp dụng phương trình từ đạo hàm Larange lúc nãy, ta có $$w$$ và $$b$$ được tính như sau: + +$$w^* = \sum_{i=1}^{n}\alpha_i^* y_n \phi(x_n)$$ + +Dựa vào phương trình trên, ta quan sát thấy $$w^*$$ chỉ bị ảnh hưởng khi $$\alpha_i > 0$$ và sẽ không bị ảnh hưởng khi $$\alpha_i = 0$$. Ta gọi những training data $$x_i$$ có $$\alpha > 0$$ là **support vectors** do chỉ các training data này mới có tác động. + +Nhớ lại ở đầu mục, chúng ta đã set điều kiện $$w^T\phi(x_{sv}) + b = y_i = \pm 1$$. Vì vậy sau khi biết $$w$$ và biết được các support vectors, ta sẽ áp dụng để tìm $$b$$. + +$$b = y_i - {w^*}^T \phi(x_{SV}) = y_i - \sum_{i=1}^{n}\alpha_i^* y_n k(x_n, x_{SV})$$ + +Chỉ cần chọn một support vector là chúng ta có thể tìm ra $$b$$. + +Và sau khi tìm ra $$w$$ và $$b$$, chúng ta đã tìm ra được hyperplane tối ưu nhất để phân tách được 2 class. + + +### 3. Code SVM + +Ở phần này, mình sẽ code SVM chỉ dùng các thư viện numpy (tính toán ma trận), cvxopt (Tối ưu), matplotlib (visualise) + +* **Bước 1: Import các thư viện cần thiết** + +```python + +import numpy as np +import cvxopt +import matplotlib.pyplot as plt +``` + +* **Bước 2: Viết các kernelisation functions** + +Có nhiều hàm kernelisation. Tuy nhiên, trong nội dung bài post, mình sẽ chỉ dùng gaussian (hay còn được gọi là RBF) và đây cũng là kernel phổ biến nhất khi SVM được dùng. + +```python +def linear(x, z): + return np.dot(x, z.T) + +def polynomial(x, z, p=5): + return (1 + np.dot(x, z.T))**p + +def gaussian(x, z, sigma=0.1): + return np.exp(-np.linalg.norm(x-z, axis=1)**2/(2*(sigma**2))) +``` + +* **Bước 3: Viết code training, predict ** + +```python +class SVM: + def __init__(self, kernel=gaussian, C=1): + self.kernel = kernel + self.C = C + + def fit(self, X, y): + self.y = y + self.X = X + m, n = X.shape + + # Calculate Kernel + self.K = np.zeros((m, m)) + for i in range(m): + self.K[i, :] = self.kernel(X[i, np.newaxis], self.X) + + # Solve with cvxopt final QP needs to be reformulated + # to match the input form for cvxopt.solvers.qp + P = cvxopt.matrix(np.outer(y, y) * self.K) + q = cvxopt.matrix(-np.ones((m, 1))) + G = cvxopt.matrix(np.vstack((np.eye(m) * -1, np.eye(m)))) + h = cvxopt.matrix(np.hstack((np.zeros(m), np.ones(m) * self.C))) + A = cvxopt.matrix(y, (1, m), "d") + b = cvxopt.matrix(np.zeros(1)) + cvxopt.solvers.options["show_progress"] = False + sol = cvxopt.solvers.qp(P, q, G, h, A, b) + self.alphas = np.array(sol["x"]) + + def predict(self, X): + y_predict = np.zeros((X.shape[0])) + sv = self.get_parameters(self.alphas) + for i in range(X.shape[0]): + y_predict[i] = np.sum( + self.alphas[sv] + * self.y[sv, np.newaxis] + * self.kernel(X[i], self.X[sv])[:, np.newaxis] + ) + return np.sign(y_predict + self.b) + + def get_parameters(self, alphas): + threshold = 1e-5 + + sv = ((alphas > threshold) * (alphas < self.C)).flatten() + self.w = np.dot(self.X[sv].T, alphas[sv] * self.y[sv, np.newaxis]) + self.b = np.mean( + self.y[sv, np.newaxis] + - self.alphas[sv] * self.y[sv, np.newaxis] * self.K[sv, sv][:, np.newaxis] + ) + return sv +``` + +* **Bước 4: Viết các hàm helpers**: + +```python +def create_dataset(N, D=2, K=2): + X = np.zeros((N * K, D)) # data matrix (each row = single example) + y = np.zeros(N * K) # class labels + + for j in range(K): + ix = range(N * j, N * (j + 1)) + r = np.linspace(0.0, 1, N) # radius + t = np.linspace(j * 4, (j + 1) * 4, N) + np.random.randn(N) * 0.2 # theta + X[ix] = np.c_[r * np.sin(t), r * np.cos(t)] + y[ix] = j + + # lets visualize the data: + plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral) + plt.show() + + y[y == 0] -= 1 + + return X, y + + +def plot_contour(X, y, svm): + # plot the resulting classifier + h = 0.01 + x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 + y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 + + xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) + + points = np.c_[xx.ravel(), yy.ravel()] + + Z = svm.predict(points) + Z = Z.reshape(xx.shape) + plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral, alpha=0.8) + + # plt the points + plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral) + plt.show() +``` + +* **Bước 4: Tạo data giả và train SVM để đánh giá** + +```python +np.random.seed(2) +X, y = create_dataset(N=1000) +permuted_idxes = np.random.permutation(len(X)) +X = X[permuted_idxes] +y = y[permuted_idxes] + +val_split = 50 +X_train, Y_train = X[:val_split], y[:val_split] +X_val, Y_val = X[val_split:], y[val_split:] + +svm = SVM(kernel=gaussian, C=0.5) +svm.fit(X_train, Y_train) +y_pred = svm.predict(X_val) + +print(f"Accuracy: {sum(Y_val==y_pred)/Y_val.shape[0]}") +``` + +```bash +Accuracy: 0.9384615384615385 +``` + +```python +from sklearn.svm import SVC +clf = SVC() +clf.fit(X_train, Y_train) +pred = clf.predict(X_val) +print(f"Accuracy: {len(pred[pred==Y_val])/(Y_val.shape[0])}") +``` + +```bash +Accuracy: 0.981025641025641 +``` + +### 4. Kết luận + +Support Vector Machine là một giải thuật binary classification được hình thành dựa trên các định lý và công thức toán học về vector, matrix, và tối ưu. Giải thuật này đã được ra đời khá lâu và hiện tại đã có rất nhiều giải thuật machine learning khác cho kết quả vượt trội hơn, tuy nhiên các giá trị toán học nền tảng cho người mới bắt đầu là rất, rất nhiều. Vì vậy, ở các trường đại học trong các môn liên quan đến machine learning, SVM vẫn luôn được dạy. + +Hy vọng các bạn cảm thấy bài viết hữu ích. Nếu có chỗ nào chưa rõ, hãy email và mình sẽ trả lời. Chúc các bạn học tốt. + + + + +### References +1\. [SVM playlist (Youtube) - Prof. Cynthia Rudin][SVM playlist] +2\. [Support Vector Machines - THE MATH YOU SHOULD KNOW (Youtube)][SVM-CodeEmporium] +3\. [Understanding the mathematics behind Support Vector Machines][Blog] + + +[SVM playlist]: https://pabloinsente.github.io/the-convolutional-network +[SVM-CodeEmporium]: https://www.youtube.com/watch?v=05VABNfa1ds +[Blog]: https://shuzhanfan.github.io/2018/05/understanding-mathematics-behind-support-vector-machines/ \ No newline at end of file diff --git a/_posts/2024-09-29-stable_diffusion.md b/_posts/2024-09-29-stable_diffusion.md new file mode 100644 index 00000000000..695b7e221c7 --- /dev/null +++ b/_posts/2024-09-29-stable_diffusion.md @@ -0,0 +1,530 @@ +--- +title: "Giải thích stable diffusion models" +mathjax: true +layout: post +categories: media +--- + +Xin chào các bạn, + +Cho đến nay, mình đã viết về hai loại mô hình sinh dữ liệu, gồm GANs và VAE. Chúng đã cho thấy thành công lớn trong việc tạo ra các hình ảnh có chất lượng rất tốt, nhưng mỗi loại đều có những hạn chế riêng. Mô hình GAN thường gặp phải vấn đề bất ổn trong quá trình training và thiếu đa dạng trong quá trình sinh dữ liệu do bản chất adversarial learning của nó. VAE dựa vào một hàm loss xấp xỉ (surrogate loss). + +Mô hình Diffusion được lấy cảm hứng từ nhiệt động học phi cân bằng (Non-equilibrium thermodynamics). Chúng áp dụng Markov chain của các bước khuếch tán nhằm dần dần thêm random noise vào data, sau đó học cách đảo ngược quá trình khuếch tán để tạo ra các mẫu dữ liệu mong muốn từ nhiễu. Không giống như GANs và VAE, mô hình Diffusion được huấn luyện theo một quy trình cố định và latent dimension có dimension bằng với data. + +
+ +
+ +### 1. Mô hình Diffusion là gì và nó hoạt động như thế nào ? + + +Stable Diffusion là một loại mô hình tạo sinh dựa trên quá trình khuếch tán, hoạt động thông qua hai bước chính: forward process và reverse process. + +Trong lúc training, diffusion được chia thành 2 quá trình: + +* **Forward process**: Dữ liệu gốc, như hình ảnh, dần bị thêm nhiễu qua nhiều bước. Mỗi bước làm cho dữ liệu trở nên ngày càng ngẫu nhiên hơn, đến khi đạt trạng thái gần giống như nhiễu thuần túy. Quá trình này được thực hiện theo chuỗi Markov, và mô hình không học trong giai đoạn này. + +
+ +
+ + +* **Reverse process**: Mô hình học cách đảo ngược quá trình nhiễu, tái tạo lại dữ liệu gốc từ nhiễu. Thông qua mạng neural networks, mô hình predict các bước khử nhiễu ngược lại từng bước một, từ nhiễu ngẫu nhiên trở về dạng dữ liệu có cấu trúc, như hình ảnh rõ ràng. + +
+ +
+ +Trong lúc inference, ta **chỉ sử dụng reverse process** bằng việc đưa vào nhiễu Gauss và mô hình sẽ trả kết quả là một tấm ảnh bất kì được sinh ra. + + +### 2. Giải thích toán học + +#### 2.1. Forward process + +Với một điểm dữ liệu được lấy mẫu từ phân phối dữ liệu thực $$x_0 \sim q(x)$$, chúng ta tiến hành _forward process_ bằng cách thêm một lượng nhỏ nhiễu Gaussian vào mẫu qua $$T$$ bước, tạo ra một chuỗi các mẫu bị nhiễu với cường độ tăng dần. Mức độ nhiễu ta thêm vào sẽ được định nghĩa bởi một scheduler $$\{ \beta_t \in (0, 1) \}_{t=1}^T$$. + +$$q(\mathbf{x}_t \vert \mathbf{x}_{t-1}) = \mathcal{N}(\mathbf{x}_t; \sqrt{1 - \beta_t} \mathbf{x}_{t-1}, \beta_t\mathbf{I}) \quad +q(\mathbf{x}_{1:T} \vert \mathbf{x}_0) = \prod^T_{t=1} q(\mathbf{x}_t \vert \mathbf{x}_{t-1})$$ + +Mẫu dữ liệu $$x_0$$ dần mất đi các đặc điểm nhận dạng khi timestep $$t$$ lớn dần. Cuối cùng, khi $$T \rightarrow \infty$$, $$x_T$$ tương đương với một phân phối Gaussian đẳng hướng (isotropic Gaussian). + +Một tính chất thú vị của quá trình trên là chúng ta có thể lấy mẫu $$x_t$$ tại bất kỳ bước time step $$t$$ nào bằng cách sử dụng **reparameterisation trick**. Đặt $$\alpha_t = 1 - \beta_t$$, và $$\bar{\alpha}_t = \prod^t_{i=1} \alpha_i$$. + +$$\begin{aligned} +\mathbf{x}_t +&= \sqrt{\alpha_t}\mathbf{x}_{t-1} + \sqrt{1 - \alpha_t}\boldsymbol{\epsilon}_{t-1} & \text{ ;Với } \boldsymbol{\epsilon}_{t-1}, \boldsymbol{\epsilon}_{t-2}, \dots \sim \mathcal{N}(\mathbf{0}, \mathbf{I}) \\ +&= \sqrt{\alpha_t \alpha_{t-1}} \mathbf{x}_{t-2} + \sqrt{1 - \alpha_t \alpha_{t-1}} \bar{\boldsymbol{\epsilon}}_{t-2} & \text{ ;với } \bar{\boldsymbol{\epsilon}}_{t-2} \text{ là hợp của 2 phân phối Gauss (*).} \\ +&= \dots \\ +&= \sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon} \\ +q(\mathbf{x}_t \vert \mathbf{x}_0) &= \mathcal{N}(\mathbf{x}_t; \sqrt{\bar{\alpha}_t} \mathbf{x}_0, (1 - \bar{\alpha}_t)\mathbf{I}) +\end{aligned}$$ + +(*) Khi chúng ta gộp hai phân phối Gaussian với phương sai khác nhau $$\mathcal{N}(\mathbf{0}, \sigma_1^2\mathbf{I})$$ và $$\mathcal{N}(\mathbf{0}, \sigma_2^2\mathbf{I})$$ phân phối mới sẽ là $$\mathcal{N}(\mathbf{0}, (\sigma_1^2 + \sigma_2^2)\mathbf{I})$$. Ở đây, độ lệch chuẩn sau khi gộp là $$\sqrt{(1 - \alpha_t) + \alpha_t (1-\alpha_{t-1})} = \sqrt{1 - \alpha_t\alpha_{t-1}}$$. + +#### 2.2. Reverse process + +Nếu chúng ta có thể đảo ngược quá trình trên và lấy mẫu từ $$q(\mathbf{x}_{t-1} \vert \mathbf{x}_t)$$, chúng ta sẽ có thể tái tạo mẫu thực từ đầu vào nhiễu Gaussian, $$\mathbf{x}_T \sim \mathcal{N}(\mathbf{0}, \mathbf{I})$$. Lưu ý rằng nếu $$\beta_t$$ đủ nhỏ, $$q(\mathbf{x}_{t-1} \vert \mathbf{x}_t)$$ cũng sẽ là Gaussian. Tuy nhiên, chúng ta không thể dễ dàng ước lượng $$q(\mathbf{x}_{t-1} \vert \mathbf{x}_t)$$ vì nó cần tính prior $$q(x_t)$$ và việc này **intractable**, do đó chúng ta cần học một mô hình $$p_\theta$$ để xấp xỉ các xác suất có điều kiện này nhằm thực hiện quá trình reverse mà không cần phải tính prior. + +$$p_\theta(\mathbf{x}_{0:T}) = p(\mathbf{x}_T) \prod^T_{t=1} p_\theta(\mathbf{x}_{t-1} \vert \mathbf{x}_t) \quad +p_\theta(\mathbf{x}_{t-1} \vert \mathbf{x}_t) = \mathcal{N}(\mathbf{x}_{t-1}; \boldsymbol{\mu}_\theta(\mathbf{x}_t, t), \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t))$$ + +
+ +
+ +Tuy nhiên, khi chỉ condition trên timestep $$t-1$$ sẽ có thể có rất nhiều uncertainty vì từ một bưc ảnh đầy nhiễu hoặc mờ mờ nhiễu, chúng ta sẽ có thể có rất nhiều kết quả tạo sinh không mong muốn. Và nếu có thể condition trên cả $$x_0$$ thì điều này sẽ làm giảm đáng kể uncertainty. Tuy nhiên, khi condition với $$x_0$$ thì công thức toán sẽ phức tạp hơn nhưng trong trường hợp này thì bài toán của chúng ta vẫn có thể giải được. Cụ thể, tác giả diễn giải như sau: + +$$q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0) = \mathcal{N}(\mathbf{x}_{t-1}; {\tilde{\boldsymbol{\mu}}}(\mathbf{x}_t, \mathbf{x}_0), {\tilde{\beta}_t} \mathbf{I})$$ + +Áp dụng định lý Bayes, ta được: + +$$\begin{aligned} +q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0) +&= q(\mathbf{x}_t \vert \mathbf{x}_{t-1}, \mathbf{x}_0) \frac{ q(\mathbf{x}_{t-1} \vert \mathbf{x}_0) }{ q(\mathbf{x}_t \vert \mathbf{x}_0) } \\ +&\propto \exp \Big(-\frac{1}{2} \big(\frac{(\mathbf{x}_t - \sqrt{\alpha_t} \mathbf{x}_{t-1})^2}{\beta_t} + \frac{(\mathbf{x}_{t-1} - \sqrt{\bar{\alpha}_{t-1}} \mathbf{x}_0)^2}{1-\bar{\alpha}_{t-1}} - \frac{(\mathbf{x}_t - \sqrt{\bar{\alpha}_t} \mathbf{x}_0)^2}{1-\bar{\alpha}_t} \big) \Big) \\ +&= \exp \Big(-\frac{1}{2} \big(\frac{\mathbf{x}_t^2 - 2\sqrt{\alpha_t} \mathbf{x}_t {\mathbf{x}_{t-1}} {+ \alpha_t} {\mathbf{x}_{t-1}^2} }{\beta_t} + \frac{ {\mathbf{x}_{t-1}^2} {- 2 \sqrt{\bar{\alpha}_{t-1}} \mathbf{x}_0} {\mathbf{x}_{t-1}} {+ \bar{\alpha}_{t-1} \mathbf{x}_0^2} }{1-\bar{\alpha}_{t-1}} - \frac{(\mathbf{x}_t - \sqrt{\bar{\alpha}_t} \mathbf{x}_0)^2}{1-\bar{\alpha}_t} \big) \Big) \\ +&= \exp\Big( -\frac{1}{2} \big( {(\frac{\alpha_t}{\beta_t} + \frac{1}{1 - \bar{\alpha}_{t-1}})} \mathbf{x}_{t-1}^2 - {(\frac{2\sqrt{\alpha_t}}{\beta_t} \mathbf{x}_t + \frac{2\sqrt{\bar{\alpha}_{t-1}}}{1 - \bar{\alpha}_{t-1}} \mathbf{x}_0)} \mathbf{x}_{t-1} { + C(\mathbf{x}_t, \mathbf{x}_0) \big) \Big)} +\end{aligned} +$$ + +Trong đó $$C(\mathbf{x}_t, \mathbf{x}_0)$$ là một hàm không liên quan đến $$\mathbf{x}_{t-1}$$ và bị lược bỏ. Theo hàm mật độ của phân phối Gaussian chuẩn, giá trị trung bình và phương sai có thể được tham số hóa như sau (nhớ rằng $$\alpha_t = 1 - \beta_t$$ và $$\bar{\alpha}_t = \prod_{i=1}^T \alpha_i$$): + +$$\begin{aligned} +\tilde{\beta}_t +&= 1/(\frac{\alpha_t}{\beta_t} + \frac{1}{1 - \bar{\alpha}_{t-1}}) += 1/(\frac{\alpha_t - \bar{\alpha}_t + \beta_t}{\beta_t(1 - \bar{\alpha}_{t-1})}) += {\frac{1 - \bar{\alpha}_{t-1}}{1 - \bar{\alpha}_t} \cdot \beta_t} \\ +\tilde{\boldsymbol{\mu}}_t (\mathbf{x}_t, \mathbf{x}_0) +&= (\frac{\sqrt{\alpha_t}}{\beta_t} \mathbf{x}_t + \frac{\sqrt{\bar{\alpha}_{t-1} }}{1 - \bar{\alpha}_{t-1}} \mathbf{x}_0)/(\frac{\alpha_t}{\beta_t} + \frac{1}{1 - \bar{\alpha}_{t-1}}) \\ +&= (\frac{\sqrt{\alpha_t}}{\beta_t} \mathbf{x}_t + \frac{\sqrt{\bar{\alpha}_{t-1} }}{1 - \bar{\alpha}_{t-1}} \mathbf{x}_0) {\frac{1 - \bar{\alpha}_{t-1}}{1 - \bar{\alpha}_t} \cdot \beta_t} \\ +&= \frac{\sqrt{\alpha_t}(1 - \bar{\alpha}_{t-1})}{1 - \bar{\alpha}_t} \mathbf{x}_t + \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1 - \bar{\alpha}_t} \mathbf{x}_0\\ +\end{aligned}$$ + +Nhờ vào tính chất thú vị này, chúng ta có thể biểu diễn $$\mathbf{x}_0 = \frac{1}{\sqrt{\bar{\alpha}_t}}(\mathbf{x}_t - \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}_t)$$ và thay vào phương trình trên để thu được: + +$$\begin{aligned} +\tilde{\boldsymbol{\mu}}_t +&= \frac{\sqrt{\alpha_t}(1 - \bar{\alpha}_{t-1})}{1 - \bar{\alpha}_t} \mathbf{x}_t + \frac{\sqrt{\bar{\alpha}_{t-1}}\beta_t}{1 - \bar{\alpha}_t} \frac{1}{\sqrt{\bar{\alpha}_t}}(\mathbf{x}_t - \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}_t) \\ +&= {\frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\epsilon}_t \Big)} +\end{aligned}$$ + +#### 2.3. Loss function + +Hàm loss của stable diffusion khá ngắn gọn, $$- \log p_\theta(\mathbf{x}_0) $$. Với hàm loss này, ta phải train một mô hình theo giải thuật như trên (forward-reverse process) và tạo ra được data mà có log-likelihood với lại tập training data cao. Nói cách khác, nếu ảnh được sinh ra có phân phối giống với training data thì hàm loss kia sẽ thấp và ngược lại. + +Biến đổi một chút, ta sẽ có được cách tính của hàm loss trên: + +$$p_\theta(x_0) = \int p_\theta(x_{0:T}) dx_{1:T}$$ + +Để tính được hàm loss, chúng ta phải tính tích phân trên tất cả các time step $$x_1, x_2, ..., x_{T-1}, x_{T-2}$$. Và việc tính này sẽ càng trở nên intractable nếu ảnh được gen ra có độ phân giải lớn và số lượng timesteps nhiều. + + +Với cách dùng Evidence Lower Bound và biến đổi, chúng ta sẽ không cần phải tính tích phân trên một miền rộng lớn như vậy và việc này biến bài toán trở nên tractable (thú vị không nào ^_^). Bài toán được biến đổi như sau: + +$$\begin{aligned} +- \log p_\theta(\mathbf{x}_0) +&\leq - \log p_\theta(\mathbf{x}_0) + D_\text{KL}(q(\mathbf{x}_{1:T}\vert\mathbf{x}_0) \| p_\theta(\mathbf{x}_{1:T}\vert\mathbf{x}_0) ) \\ +&= -\log p_\theta(\mathbf{x}_0) + \mathbb{E}_{\mathbf{x}_{1:T}\sim q(\mathbf{x}_{1:T} \vert \mathbf{x}_0)} \Big[ \log\frac{q(\mathbf{x}_{1:T}\vert\mathbf{x}_0)}{p_\theta(\mathbf{x}_{0:T}) / p_\theta(\mathbf{x}_0)} \Big] \\ +&= -\log p_\theta(\mathbf{x}_0) + \mathbb{E}_q \Big[ \log\frac{q(\mathbf{x}_{1:T}\vert\mathbf{x}_0)}{p_\theta(\mathbf{x}_{0:T})} + \log p_\theta(\mathbf{x}_0) \Big] \\ +&= \mathbb{E}_q \Big[ \log \frac{q(\mathbf{x}_{1:T}\vert\mathbf{x}_0)}{p_\theta(\mathbf{x}_{0:T})} \Big] \\ +\text{Let }L_\text{VLB} +&= \mathbb{E}_{q(\mathbf{x}_{0:T})} \Big[ \log \frac{q(\mathbf{x}_{1:T}\vert\mathbf{x}_0)}{p_\theta(\mathbf{x}_{0:T})} \Big] \geq - \mathbb{E}_{q(\mathbf{x}_0)} \log p_\theta(\mathbf{x}_0) +\end{aligned}$$ + +$$\begin{aligned} +L_\text{VLB} +&= \mathbb{E}_{q(\mathbf{x}_{0:T})} \Big[ \log\frac{q(\mathbf{x}_{1:T}\vert\mathbf{x}_0)}{p_\theta(\mathbf{x}_{0:T})} \Big] \\ +&= \mathbb{E}_q \Big[ \log\frac{\prod_{t=1}^T q(\mathbf{x}_t\vert\mathbf{x}_{t-1})}{ p_\theta(\mathbf{x}_T) \prod_{t=1}^T p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t) } \Big] \\ +&= \mathbb{E}_q \Big[ -\log p_\theta(\mathbf{x}_T) + \sum_{t=1}^T \log \frac{q(\mathbf{x}_t\vert\mathbf{x}_{t-1})}{p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t)} \Big] \\ +&= \mathbb{E}_q \Big[ -\log p_\theta(\mathbf{x}_T) + \sum_{t=2}^T \log \frac{q(\mathbf{x}_t\vert\mathbf{x}_{t-1})}{p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t)} + \log\frac{q(\mathbf{x}_1 \vert \mathbf{x}_0)}{p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1)} \Big] \\ +&= \mathbb{E}_q \Big[ -\log p_\theta(\mathbf{x}_T) + \sum_{t=2}^T \log \Big( \frac{q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0)}{p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t)}\cdot \frac{q(\mathbf{x}_t \vert \mathbf{x}_0)}{q(\mathbf{x}_{t-1}\vert\mathbf{x}_0)} \Big) + \log \frac{q(\mathbf{x}_1 \vert \mathbf{x}_0)}{p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1)} \Big] \\ +&= \mathbb{E}_q \Big[ -\log p_\theta(\mathbf{x}_T) + \sum_{t=2}^T \log \frac{q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0)}{p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t)} + \sum_{t=2}^T \log \frac{q(\mathbf{x}_t \vert \mathbf{x}_0)}{q(\mathbf{x}_{t-1} \vert \mathbf{x}_0)} + \log\frac{q(\mathbf{x}_1 \vert \mathbf{x}_0)}{p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1)} \Big] \\ +&= \mathbb{E}_q \Big[ -\log p_\theta(\mathbf{x}_T) + \sum_{t=2}^T \log \frac{q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0)}{p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t)} + \log\frac{q(\mathbf{x}_T \vert \mathbf{x}_0)}{q(\mathbf{x}_1 \vert \mathbf{x}_0)} + \log \frac{q(\mathbf{x}_1 \vert \mathbf{x}_0)}{p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1)} \Big]\\ +&= \mathbb{E}_q \Big[ \log\frac{q(\mathbf{x}_T \vert \mathbf{x}_0)}{p_\theta(\mathbf{x}_T)} + \sum_{t=2}^T \log \frac{q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0)}{p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t)} - \log p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1) \Big] \\ +&= \mathbb{E}_q [\underbrace{D_\text{KL}(q(\mathbf{x}_T \vert \mathbf{x}_0) \parallel p_\theta(\mathbf{x}_T))}_{L_T} + \sum_{t=2}^T \underbrace{D_\text{KL}(q(\mathbf{x}_{t-1} \vert \mathbf{x}_t, \mathbf{x}_0) \parallel p_\theta(\mathbf{x}_{t-1} \vert\mathbf{x}_t))}_{L_{t-1}} \underbrace{- \log p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1)}_{L_0} ] +\end{aligned}$$ + +$$\begin{aligned} +L_\text{VLB} &= L_T + L_{T-1} + \dots + L_0 \\ +\text{where } L_T &= D_\text{KL}(q(\mathbf{x}_T \vert \mathbf{x}_0) \parallel p_\theta(\mathbf{x}_T)) \\ +L_t &= D_\text{KL}(q(\mathbf{x}_t \vert \mathbf{x}_{t+1}, \mathbf{x}_0) \parallel p_\theta(\mathbf{x}_t \vert\mathbf{x}_{t+1})) \text{ for }1 \leq t \leq T-1 \\ +L_0 &= - \log p_\theta(\mathbf{x}_0 \vert \mathbf{x}_1) +\end{aligned}$$ + +Mỗi thành phần KL trong $$L_\text{VLB}$$ (ngoại trừ $$L_0$$) so sánh hai phân phối Gaussian và do đó chúng có thể được tính toán dưới dạng closed-form. $$L_T$$ là hằng số và có thể bị bỏ qua trong quá trình huấn luyện vì $$q$$ không có tham số có thể học được và là một nhiễu Gaussian. + +Hãy nhớ rằng chúng ta cần học một mạng neural để xấp xỉ các phân phối xác suất có điều kiện trong reverse process, $$p_\theta(\mathbf{x}_{t-1} \vert \mathbf{x}_t) = \mathcal{N}(\mathbf{x}_{t-1}; \boldsymbol{\mu}_\theta(\mathbf{x}_t, t), \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t))$$. Chúng ta muốn huấn luyện $$\boldsymbol{\mu}_\theta$$ để dự đoán $$\tilde{\boldsymbol{\mu}}_t = \frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\epsilon}_t \Big)$$. Vì $$\mathbf{x}_t$$ có sẵn như đầu vào trong thời gian huấn luyện, chúng ta có thể tái tham số hóa thành phần nhiễu Gaussian thay vào đó để nó dự đoán $$\boldsymbol{\epsilon}_t$$ từ đầu vào $$\mathbf{x}_t$$ tại timestep $$t$$: + +$$\begin{aligned} +\boldsymbol{\mu}_\theta(\mathbf{x}_t, t) &= {\frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \Big)} \\ +\text{Thus }\mathbf{x}_{t-1} &= \mathcal{N}(\mathbf{x}_{t-1}; \frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \Big), \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t)) +\end{aligned}$$ + +Hàm loss $$L_t$$ được tham số hóa để minimise sự khác biệt từ $$\tilde{\boldsymbol{\mu}}$$ bằng cách sử dụng L2 loss và hệ số $$\frac{1}{2 \sigma ^2}$$ cho việc scaling. Các bạn cũng có thể sử dụng nhiều loại khác như L1 loss, Huber loss, Entropy loss, ... Phổ biến nhất vẫn dùng là L2 loss. + +$$\begin{aligned} +L_t +&= \mathbb{E}_{\mathbf{x}_0, \boldsymbol{\epsilon}} \Big[\frac{1}{2 \| \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t) \|^2_2} \| {\tilde{\boldsymbol{\mu}}_t(\mathbf{x}_t, \mathbf{x}_0)} - {\boldsymbol{\mu}_\theta(\mathbf{x}_t, t)} \|^2 \Big] \\ +&= \mathbb{E}_{\mathbf{x}_0, \boldsymbol{\epsilon}} \Big[\frac{1}{2 \|\boldsymbol{\Sigma}_\theta \|^2_2} \| {\frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\epsilon}_t \Big)} - {\frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\boldsymbol{\epsilon}}_\theta(\mathbf{x}_t, t) \Big)} \|^2 \Big] \\ +&= \mathbb{E}_{\mathbf{x}_0, \boldsymbol{\epsilon}} \Big[\frac{ (1 - \alpha_t)^2 }{2 \alpha_t (1 - \bar{\alpha}_t) \| \boldsymbol{\Sigma}_\theta \|^2_2} \|\boldsymbol{\epsilon}_t - \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\|^2 \Big] \\ +&= \mathbb{E}_{\mathbf{x}_0, \boldsymbol{\epsilon}} \Big[\frac{ (1 - \alpha_t)^2 }{2 \alpha_t (1 - \bar{\alpha}_t) \| \boldsymbol{\Sigma}_\theta \|^2_2} \|\boldsymbol{\epsilon}_t - \boldsymbol{\epsilon}_\theta(\sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}_t, t)\|^2 \Big] +\end{aligned}$$ + +Thực nghiệm cho thấy việc huấn luyện mô hình diffusion hoạt động hiệu quả hơn với việc lược đi trọng số: + +$$\begin{aligned} +L_t^\text{simple} +&= \mathbb{E}_{t \sim [1, T], \mathbf{x}_0, \boldsymbol{\epsilon}_t} \Big[\|\boldsymbol{\epsilon}_t - \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)\|^2 \Big] \\ +&= \mathbb{E}_{t \sim [1, T], \mathbf{x}_0, \boldsymbol{\epsilon}_t} \Big[\|\boldsymbol{\epsilon}_t - \boldsymbol{\epsilon}_\theta(\sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}_t, t)\|^2 \Big] +\end{aligned}$$ + +Lưu ý rằng hàm loss cuối cùng này đã bao gồm cả $$L_0$$ ở trên. Do về mặt bản chất, $$L_0$$ cũng là đo khoảng cách KL giữa training data ban đầu và data được sinh với đầu vào là $$x_1$$. + + +### 3. Implementation + +* **Step 1**: Import các thư viện cần thiết + +```python +import torch +import torchvision +import matplotlib.pyplot as plt +import torch.nn.functional as F +from torch import nn +import math +from torchvision import transforms +from torch.utils.data import DataLoader +import numpy as np +from torch.optim import Adam +from tqdm import tqdm +``` + +* **Step 1**: Download dataset và tạo dataloader + +```python +IMG_SIZE = 28 +BATCH_SIZE = 128 + +def load_transformed_dataset(): + data_transforms = [ + transforms.Resize((IMG_SIZE, IMG_SIZE)), + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), # Scales data into [0,1] + transforms.Lambda(lambda t: (t * 2) - 1) # Scale between [-1, 1] + ] + data_transform = transforms.Compose(data_transforms) + + train = torchvision.datasets.MNIST(root=".", download=True, + transform=data_transform, train=True) + + test = torchvision.datasets.MNIST(root=".", download=True, + transform=data_transform, train=False) + return torch.utils.data.ConcatDataset([train, test]) + +data = load_transformed_dataset() +dataloader = DataLoader(data, batch_size=BATCH_SIZE, shuffle=True, drop_last=True) +``` + +* **Step 3**: Các functions hỗ trợ cho stable diffusion + +```python +def linear_beta_schedule(timesteps: float, start: float = 0.0001, end: float = 0.02): + return torch.linspace(start, end, timesteps) + +def get_index_from_list(vals, t, x_shape): + """ + Returns a specific index t of a passed list of values vals + while considering the batch dimension. + """ + batch_size = t.shape[0] + out = vals.gather(-1, t.cpu()) + return out.reshape(batch_size, *((1,) * (len(x_shape) - 1))).to(t.device) +``` + +Bước forward của stable diffusion sẽ được implement theo công thức: + +$$\mathbf{x}_t = \sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon}$$ + +```python +def forward_diffusion_sample(x_0: torch.Tensor, t: torch.Tensor, device="cpu"): + """ + Closed-form forward diffusion step processed in batches. + """ + noise = torch.randn_like(x_0) + sqrt_alphas_cumprod_t = get_index_from_list(sqrt_alphas_cumprod, t, x_0.shape) + sqrt_one_minus_alphas_cumprod_t = get_index_from_list( + sqrt_one_minus_alphas_cumprod, t, x_0.shape + ) + # mean + variance + return sqrt_alphas_cumprod_t.to(device) * x_0.to(device) \ + + sqrt_one_minus_alphas_cumprod_t.to(device) * noise.to(device), noise.to(device) +``` + +* **Step 4**: Build U-Net + +```python +class Block(nn.Module): + def __init__(self, in_ch, out_ch, time_emb_dim, up=False): + super().__init__() + self.time_mlp = nn.Linear(time_emb_dim, out_ch) + if up: + self.conv1 = nn.Conv2d(in_ch + out_ch, out_ch, 3, padding=1) # Supports skip connection + self.transform = nn.ConvTranspose2d(out_ch, out_ch, 4, 2, 1) + else: + self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1) + self.transform = nn.Conv2d(out_ch, out_ch, 4, 2, 1) + self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1) + self.bnorm1 = nn.BatchNorm2d(out_ch) + self.bnorm2 = nn.BatchNorm2d(out_ch) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x, t, skip=None): + # First Conv + Time embedding + h = self.bnorm1(self.relu(self.conv1(x))) + time_emb = self.relu(self.time_mlp(t)) + time_emb = time_emb[(...,) + (None,) * 2] + h = h + time_emb + + # Skip connection if provided + if skip is not None: + h = torch.cat((h, skip), dim=1) + + # Second Conv + h = self.bnorm2(self.relu(self.conv2(h))) + + # Down or Upsample + return self.transform(h) + +class SinusoidalPositionEmbeddings(nn.Module): + def __init__(self, dim): + super().__init__() + self.dim = dim + + def forward(self, time): + device = time.device + half_dim = self.dim // 2 + embeddings = math.log(1e4) / (half_dim - 1) + embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings) + embeddings = time[:, None] * embeddings[None, :] + embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1) + return embeddings + +class Block(nn.Module): + def __init__(self, in_ch, out_ch, time_emb_dim, up=False): + super().__init__() + self.time_mlp = nn.Linear(time_emb_dim, out_ch) + self.up = up + if up: + self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1) + self.transform = nn.ConvTranspose2d(out_ch, out_ch, 4, 2, 1) + else: + self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1) + self.transform = nn.Conv2d(out_ch, out_ch, 4, 2, 1) + self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1) + self.bnorm1 = nn.BatchNorm2d(out_ch) + self.bnorm2 = nn.BatchNorm2d(out_ch) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x, t): + # First Conv + h = self.bnorm1(self.relu(self.conv1(x))) + # Add time embedding + time_emb = self.relu(self.time_mlp(t)) + time_emb = time_emb[(..., ) + (None, ) * 2] + h = h + time_emb + # Second Conv + h = self.bnorm2(self.relu(self.conv2(h))) + # Add skip connection + if h.shape == x.shape: + h = h + x + # Transform + return self.transform(h) + +class SimpleUnet(nn.Module): + """ + A simplified variant of the U-Net architecture with ResNet-style skip connections. + """ + def __init__(self): + super().__init__() + image_channels = 1 + down_channels = (32, 64, 128) + up_channels = (128, 64, 32) + out_dim = 1 + time_emb_dim = 32 + + # Time embedding + self.time_mlp = nn.Sequential( + SinusoidalPositionEmbeddings(time_emb_dim), + nn.Linear(time_emb_dim, time_emb_dim), + nn.ReLU(inplace=True) + ) + + # Initial projection + self.conv0 = nn.Conv2d(image_channels, down_channels[0], 3, padding=1) + + # Downsample blocks + self.downs = nn.ModuleList([ + Block(down_channels[i], down_channels[i+1], time_emb_dim) + for i in range(len(down_channels) - 1) + ]) + + # Upsample blocks + self.ups = nn.ModuleList([ + Block(up_channels[i], up_channels[i+1], time_emb_dim, up=True) + for i in range(len(up_channels) - 1) + ]) + + # Final output layer + self.output = nn.Conv2d(up_channels[-1], out_dim, 1) + + def forward(self, x, timestep): + # Embed time + t = self.time_mlp(timestep) + + # Initial projection + x = self.conv0(x) + + # Downsampling with skip connections + skip_connections = [] + for down in self.downs: + x = down(x, t) + skip_connections.append(x) + + # Upsampling with ResNet-style skip connections + for up in self.ups: + skip_x = skip_connections.pop() + if x.shape == skip_x.shape: # Ensure shapes match for addition + x = x + skip_x + x = up(x, t) + + # Final output + return self.output(x) + + +model = SimpleUnet() +print("Num params: ", sum(p.numel() for p in model.parameters())) +``` + +```bash +Num params: 837249 +``` + +* **Step 5**: Loss function + +Mình sẽ sử dụng Huber Loss, các bạn cũng có thể các hàm loss khác mình đề xuất ở trên. + +```python +def get_loss(model, x_0, t): + x_noisy, noise = forward_diffusion_sample(x_0, t, device) + noise_pred = model(x_noisy, t) + return F.huber_loss(noise, noise_pred) +``` + +* **Step 6**: Set up và train + +```python +# Define beta schedule +T = 300 +betas = linear_beta_schedule(timesteps=T) + +# Pre-calculate different terms for closed form +alphas = 1. - betas +alphas_cumprod = torch.cumprod(alphas, axis=0) +alphas_cumprod_prev = F.pad(alphas_cumprod[:-1], (1, 0), value=1.0) +sqrt_recip_alphas = torch.sqrt(1.0 / alphas) +sqrt_alphas_cumprod = torch.sqrt(alphas_cumprod) +sqrt_one_minus_alphas_cumprod = torch.sqrt(1. - alphas_cumprod) +posterior_variance = betas * (1. - alphas_cumprod_prev) / (1. - alphas_cumprod) +``` + +```python +# Training + +device = "cuda" if torch.cuda.is_available() else "cpu" +model.to(device) +optimizer = Adam(model.parameters(), lr=0.001) +epochs = 100 + +for epoch in range(epochs): + epoch_loss = 0 + for step, batch in enumerate(tqdm(dataloader)): + optimizer.zero_grad() + t = torch.randint(0, T, (BATCH_SIZE,), device=device).long() + loss = get_loss(model, batch[0], t) + epoch_loss += loss.item() + loss.backward() + optimizer.step() + print(f"Epoch: {epoch}, Total loss: {epoch_loss}") +``` + +```bash +100%|██████████| 546/546 [00:21<00:00, 25.97it/s] +Epoch: 0, Total loss: 25.88470129109919 +100%|██████████| 546/546 [00:19<00:00, 28.07it/s] +Epoch: 1, Total loss: 14.593471519649029 +100%|██████████| 546/546 [00:19<00:00, 27.87it/s] +Epoch: 2, Total loss: 13.600953148677945 +100%|██████████| 546/546 [00:20<00:00, 27.10it/s] +Epoch: 3, Total loss: 13.152173833921552 +100%|██████████| 546/546 [00:19<00:00, 27.36it/s] +... +``` + +* **Step 7**: Inference and visualisation + +Bước inference sẽ được thực hiện theo công thức sau: + +$$\begin{aligned} +\hat{\boldsymbol{x}}_{t-1} +&= {\frac{1}{\sqrt{\alpha_t}} \Big( \mathbf{x}_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha}_t}} \boldsymbol{\epsilon}_\theta(x_t, t) \Big)} + {\frac{1 - \bar{\alpha}_{t-1}}{1 - \bar{\alpha}_t} \cdot \beta_t} \cdot \epsilon +\end{aligned}$$ + +```python +@torch.no_grad() +def sample_timestep(x, t): + """ + Calls the model to predict the noise in the image and returns + the denoised image. + Applies noise to this image, if we are not in the last step yet. + """ + betas_t = get_index_from_list(betas, t, x.shape) + sqrt_one_minus_alphas_cumprod_t = get_index_from_list( + sqrt_one_minus_alphas_cumprod, t, x.shape + ) + sqrt_recip_alphas_t = get_index_from_list(sqrt_recip_alphas, t, x.shape) + + model_mean = sqrt_recip_alphas_t * ( + x - betas_t * model(x, t) / sqrt_one_minus_alphas_cumprod_t + ) + posterior_variance_t = get_index_from_list(posterior_variance, t, x.shape) + + return model_mean + torch.sqrt(posterior_variance_t) * torch.randn_like(x) +``` + +```python +model.eval() +fully_noised_sample = torch.randn((1, 1, 28, 28)).cuda() +generated_sample = fully_noised_sample.clone() +for timestep in tqdm(range(T-1, -1, -1)): + timestep = torch.Tensor([timestep]).long().cuda() + generated_sample = sample_timestep(generated_sample, timestep) +plt.imshow(generated_sample[0, 0].detach().cpu()) +``` + +### 4. Kết luận + +Diffusion model là một mô hình sinh dữ liệu dựa trên 2 quá trình tuần tự là _forward process_ và _reverse process_. Khác với các mô hình sinh dùng adversarial training như GAN hay surrogate loss như VAE, diffusion model thực hiện quá trình thêm nhiễu dần dần vào dữ liệu, sau đó học cách đảo ngược quá trình để tái tạo lại mẫu gốc từ nhiễu. Điều này giúp mô hình có thể tuần tự và chậm rãi sinh ra kết quả, vì vậy sự ổn định cũng được cải thiện hơn nhiều so với các phương pháp trước đó. Với tiềm năng vượt trội và tính đơn giản trong kiến trúc, các mô hình diffusion đã chứng tỏ hiệu quả cao trong việc sinh data. + +Trong bài viết này, mình đã giới thiệu về diffusion model, cách hoạt động, và các chi tiết toán học của nó. Hy vọng các bạn cảm thấy hữu ích. Chúc các bạn học tốt!. + +**P/s**: Bài viết được lấy chủ yếu từ blog tuyệt vời của Lilian blog và được bổ sung thêm một vài chi tiết toán để giúp các công thức toán nhẹ nhàng hơn so với bài viết gốc. Vì vậy, xin chân thành cảm ơn Lilian Weng. + +### References +1\. [What are Diffusion Models? - Lil's blog][lil_blog] +2\. [Diffusion Models | Paper Explanation | Math Explained][Diffusion Models | Paper Explanation | Math Explained] + + +[lil_blog]: https://lilianweng.github.io/posts/2021-07-11-diffusion-models/ +[Diffusion Models | Paper Explanation | Math Explained]: https://www.youtube.com/watch?v=HoKDTa5jHvg&t=1074s \ No newline at end of file diff --git a/_posts/2024-10-03-RL_p1.md b/_posts/2024-10-03-RL_p1.md new file mode 100644 index 00000000000..5dd16dcadf3 --- /dev/null +++ b/_posts/2024-10-03-RL_p1.md @@ -0,0 +1,166 @@ +--- +title: "Reinforcement Learning - Phần 1" +mathjax: true +layout: post +categories: media +--- + + +### 1. Giới thiệu về reinforcement learning + +Reinforcement learning là một nhánh đặc biệt của học máy và **không thuộc cả 2 loại supervised learning và unsupervised learning**. Lý do nó không phải là supervised learning và unsupervised learning là vì phương pháp này không cần đến labels và không được thiết kế để tìm ra cấu trúc ẩn (hidden structure) của dữ liệu. Đây là một giải thuật học bằng trials-and-errors. Trong RL, chúng ta sẽ huấn luyện agent học cách hành động trong một môi trường (environment), ta chỉ cần đặt mục tiêu và agent sẽ **tìm ra phương án để đạt được mục tiêu đó**. + +
+ +
+ + +Ví dụ, trong cờ tướng chúng ta biết thế nào là thắng nhưng còn **làm sao để thắng** thì vẫn là ẩn số (LoL). Tương tự với Cờ vua, quay rubic, chứng khoán, Liên Minh Huyền Thoại, Fifa, đột kích, ..., chúng ta đều biết như thế nào là thắng hoặc hoàn thành nhiệm vụ, tuy nhiên con đường đến đó chúng ta chưa biết. Và giải thuật reinforcement learning sẽ giúp tìm ra lời giải cho bài toán **làm sao** này. + + +### 2. Lịch sử của reinforcement learning + +Reinforcement Learning (RL) bắt nguồn từ lý thuyết hành vi trong tâm lý học, với khái niệm thử và sai và phần thưởng từ thập niên 1950. Những nhà tiên phong như Richard Bellman đã phát triển các khái niệm nền tảng như Dynamic Programming và Bellman Equation, đặt nền móng cho lý thuyết RL. Trong những năm 1980, Watkins giới thiệu Q-learning, một phương pháp nổi tiếng giúp tác nhân học cách tối ưu hóa hành động mà không cần biết trước mô hình môi trường. + +Tuy nhiên, RL chỉ thực sự bùng nổ vào đầu thập niên 2010, khi DeepMind phát triển Deep Q Networks (DQN), kết hợp RL với mạng CNN, cho phép agent học qua hình ảnh và đánh bại con người trong trò chơi Atari. Sau đó, thành công của các hệ thống như AlphaGo, OpenAI Five, ChatGPT, và nhiều ứng dụng trong tự động hóa đã đưa RL lên tầm cao mới, trở thành lĩnh vực nổi bật trong AI. + +
+ +
Hình 1.2. AlphaGo đánh bại kỳ thủ cờ vây số 1 thế giới Lee Sedol
+
+ +### 3. Khái niệm và thuật ngữ trong RL + +Trong phần này, mình sẽ giải thích các thuật ngữ được dùng thông dụng nhất trong RL như states, actions, policies, ... + +#### 3.1. States and observations + +Một trạng thái (state) $$s$$ là mô tả hoàn chỉnh về trạng thái của agent trong môi trường. Một quan sát (observation) $$o$$ là mô tả một phần của trạng thái, có thể bỏ qua một số thông tin. Tuy nhiên, trong nhiều tài liệu, họ vẫn sử dụng state và observation một cách đồng nghĩa. + +Trong deep RL, chúng ta hầu như luôn biểu diễn các trạng thái và quan sát bằng một vector giá trị thực, ma trận hoặc tensor bậc cao hơn. Ví dụ, một quan sát hình ảnh có thể được biểu diễn bằng ma trận RGB của các giá trị pixel; trạng thái của một robot có thể được biểu diễn bởi các góc khớp và vận tốc của nó. + +Khi agent có thể quan sát toàn bộ trạng thái của môi trường, chúng ta nói rằng môi trường được quan sát đầy đủ (fully observed). Khi agent chỉ có thể thấy một phần quan sát, chúng ta nói rằng môi trường được quan sát một phần (partially observed). + +#### 3.2. Action spaces + +Các môi trường khác nhau cho phép các loại hành động khác nhau. Tập hợp tất cả các hành động hợp lệ trong một môi trường nhất định thường được gọi là action space. Một số môi trường, như Atari và Go, có không gian hành động rời rạc (discrete action space), nơi chỉ có một số lượng hữu hạn các nước đi có sẵn cho agent. Các môi trường khác, như khi tác nhân điều khiển robot trong thế giới vật lý, có không gian hành động liên tục (continuous action space). Trong các không gian liên tục, các hành động là các vector giá trị thực. + +Sự phân biệt này có những hệ quả khá sâu sắc đối với các phương pháp trong deep RL. Một số họ thuật toán chỉ có thể được áp dụng trực tiếp trong một trường hợp, và sẽ phải được sửa đổi đáng kể để áp dụng cho trường hợp khác. + +#### 3.3. Policies + +Policy là quy luật mà agent dùng nó để quyết định hành động. Nó có thể là cứng (deterministic) và được kí hiệu là $$\mu$$, + +$$a_t = \mu(s_t)$$ + +hoặc được lấy mẫu từ một distribution (stochastic) và được kí hiệu bằng $$\pi$$: + +$$a_t \sim \pi(\cdot|s_t)$$ + +Bởi vì policy như là não của agent nên cũng sẽ có nhiều blogs và papers sử dụng agent và policy với ý nghĩa như nhau. + +Với deep RL, các policy được parameterised nên chúng sẽ có notation như sau: + +$$a_t = \mu_\theta(s_t)$$ + +```python +# Deterministic, for demonstration only +mu_net = nn.Sequential( + nn.Linear(obs_dim, 64), + nn.Tanh(), + nn.Linear(64, 64), + nn.Tanh(), + nn.Linear(64, 3) + ) + +action = mu_net(state).argmax() +print(f"At state: {state}, Take action: {action}") +``` + +$$a_t \sim \pi_\theta(\cdot | s_t)$$ + +```python +# Stochastic +pi_net = nn.Sequential( + nn.Linear(obs_dim, 64), + nn.Tanh(), + nn.Linear(64, 64), + nn.Tanh(), + nn.Linear(64, 3) + ) +action_distribution = torch.softmax(pi_net(state), dim=0) + +# Sample from the categorical distribution +sampled_action = torch.multinomial(action_distribution, num_samples=1) +print(f"At state: {state}, Take action: {sampled_action}") +``` + +#### 3.4. Trajectories + +Trajectory $$\tau$$ là một chuỗi các states và actions của agent trong environment. + +$$\tau = (s_0, a_0, s_1, a_1, ...)$$ + +$$s_0$$ +là trạng thái ban đầu của agent trong environment, thường được kí hiệu bằng $$\rho_0$$: + +$$s_0 \sim \rho_0(\cdot).$$ + +Ở mỗi state bất kì, agent sẽ chọn một hành động và sau khi thực hiện hành động này thì môi trường sẽ chuyển sang một state mới và quá trình này được gọi là _state transition_. State transition được quyết định bởi môi trường, nó có thể được biểu diễn bằng hàm deterministic, + +$$s_{t+1} = f(s_t, a_t)$$ + +hoặc stochastic, + +$$s_{t+1} \sim P(\cdot|s_t, a_t)$$ + +* Note: Trajectories trong một vài tài liệu hoặc cách implementations, chúng còn được gọi là **episodes** hoặc **rollouts**. + +#### 3.5. Reward & Returns + +Reward $$R$$ là một trong những yếu tố quan trọng nhất khi bạn tự thiết kế môi trường cho agent. Nó phụ thuộc vào 3 yếu tố: trạng thái hiện tại, hành động của agent, và trạng thái tiếp theo, + +$$r_t = R(s_t, a_t, s_{t+1})$$ + +Mục tiêu của agent là làm sao để đạt được tổng tích lũy phần thưởng lớn nhất trong một trajectory. Tổng tích lũy phần thưởng này trong RL được gọi là _return_ và thông thường chúng có 2 cách biểu diễn. Một là **finite-horizon undiscounted return**, + +$$R(\tau) = \sum_{t=0}^{T}r_t$$ + +hai là **infinite-horizon discounted return** + +$$\qquad \qquad \qquad \qquad \begin{aligned} +R(\tau) = \sum_{t=0}^{\infty} \gamma^tr_t \quad \quad \text{Với } \gamma \in (0, 1) +\end{aligned}$$ + +#### 3.6. Model-free và Model-based + +Nếu bạn nhìn vào đầu mục và nghĩ tới 2 trường hợp dùng không dùng và dùng deep learning cho agent thì chưa đúng. Model-free và model-based dùng để ám chỉ liệu bài toán có hàm để biểu diễn environment hay chưa. + +Với model-free, agent không có hoặc không học mô hình của môi trường. Agent trong case model-free này được thả vào môi trường và **nhận về tín hiệu feedback từ môi trường**. Ví dụ trong trường hợp đi thi, chúng ta không biết đề thi sẽ như thế nào và cách duy nhất để biết là thi 1 lần, 2 lần, 3 lần và dần dần sẽ rút ra kinh nghiệm. + +Ngược lại, với model-based thì agent sẽ học hoặc đã có một mô hình mô tả được môi trường và biết được môi trường sẽ trả cho nó feedback gì khi nó thực hiện một hành động cụ thể. Với ví dụ trên, thì agent đã biết được đề thi sẽ hỏi những câu gì, dạng nào, ... + +| **Phương pháp** | **Điểm mạnh** | **Điểm yếu** | +|----------------------|-------------------------------------------------------------------------------|-------------------------------------------------------------------------------| +| **Model-Free RL** | - Đơn giản hơn để implement.| - Cần rất, rất nhiều trials-and-errors. | +| | - Không yêu cầu mô hình của môi trường. | - Không thể lập kế hoạch dài hạn vì không biết trước các chuyển đổi trạng thái.| +| | - Thường hiệu quả trong môi trường phức tạp và khó mô hình hóa. | | +| **Model-Based RL** | - Hiệu quả về mẫu, có thể lập kế hoạch bằng cách mô phỏng các bước trong tương lai. | - Cần một mô hình chính xác của môi trường, điều này khó học và tốn kém.| +| | - Tốt hơn trong việc planning. | - Phức tạp hơn để triển khai do yêu cầu mô hình hóa môi trường. | +| | - Có thể "tưởng tượng" các kết quả tương lai mà không cần tương tác trực tiếp | - Nếu mô hình không chính xác, có thể dẫn đến các quyết định sai lầm (Sai 1 ly, đi rất nhiều công sức và tài nguyên). | + +Với Model-based RL, chúng thường được sử dụng trong các mô phỏng hoặc games. Trong các vấn đề thực tế như áp dụng vào chứng khoán, xe tự hành thì phương pháp model-free chủ yếu được sử dụng. Và vì model-free là cách mà được sử dụng phổ biến nhất nên trong đa số các papers thì hướng nghiên cứu này có số lượng publications cao hơn rất nhiều so với model-based. + +### 4. Kết luận + +Trong bài viết trên, mình đã giới thiệu về reinforcement learning, ứng dụng, và những thuật ngữ quan trọng trong lĩnh vực này. Ở các phần tiếp theo, mình sẽ giới thiệu và implement về các giải thuật liên quan. Hy vọng các bạn cảm thấy hữu ích. + + +### References +1\. [Part 1: Key Concepts in RL - OpenAI Spinning Up][part1_openai] +2\. [A (Long) Peek into Reinforcement Learning][lilian_blog] +3\. Reinforcement Learning An Introduction by Richard S. Sutton & Andrew G. Barto + + +[part1_openai]: https://spinningup.openai.com/en/latest/spinningup/rl_intro.html +[lilian_blog]: https://lilianweng.github.io/posts/2018-02-19-rl-overview/ \ No newline at end of file diff --git a/_posts/2024-11-05-energy_based_models.md b/_posts/2024-11-05-energy_based_models.md new file mode 100644 index 00000000000..19fa482cfe6 --- /dev/null +++ b/_posts/2024-11-05-energy_based_models.md @@ -0,0 +1,183 @@ +--- +title: "Energy-based models" +mathjax: true +layout: post +categories: media +--- + +
+ +
+ +### 1. Introduction to Energy-based models (EBMs) + +Energy-based models (EBMs) are a class of probabilistic models that assign an energy to each configuration of the input data and learn to identify low-energy configurations. Unlike traditional supervised learning methods that directly predict output from input, EBMs instead use an energy function to determine stable configurations. + +* **Key Concepts:** + +* * **Energy function**: An energy function is a scalar function that maps input configurations to energy values. The goal is to find low-energy configurations, which correspond to stable states or patterns in the data. + +* * **State:** Each possible configuration of the model (e.g., the activations in a neural network) represents a state. + +* * **Energy Minimisation:** EBMs learn by finding the lowest-energy states, effectively discovering the most probable or stable states. + +* **Why EBMs are relevant:** + +* * They offer a flexible approach to model complex distributions without requiring explicit probability calculations. + +* * EBMs have foundational importance in unsupervised learning and associative memory and have influenced many modern architectures. + +### 2. Core concepts of energy-based models + +* **Energy functions:** In physics, energy represents the system's potential to perform work. In EBMs, energy functions encode how compatible a given state is with the underlying data distribution. Lower energy means the model is more confident in that state, making energy minimisation a core process. + +* **Objective:** The primary objective in EBMs is to minimise the energy of states that align with observed data, while pushing non-observed (or less probable) states to higher energies. + +* **Connection to physics**: EBMs are inspired by concepts from statistical mechanics, where systems evolve towards configurations that minimize free energy. This concept is leveraged to ensure the model "learns" representations by converging to stable, low-energy states. + + +### 3. Restricted Boltzmann Machines (RBMs) + +#### 3.1. Brief introduction to Restricted Boltzmann Machine + +Restricted Boltzmann Machine (RBM) is a generative stochastic network that can learn a probability distribution over its training data. + + +#### 3.2. Inference phase + +
+ +
+ +RBMs contain two layers: visible layer ($$v$$) and hidden ($$h$$). + +In the scope of this blog, I will use Bernoulli RBM for better explanation. In the BernoulliRBM, all units are binary stochastic units. This means that the input data should either be binary, or real-valued between 0 and 1 signifying the probability that the visible unit would turn on or off. + +The conditional probability distribution of each unit is given by the logistic sigmoid activation function of the input it receives: + +$$\begin{split}P(v_i=1|\mathbf{h}) = \sigma(\sum_j w_{ij}h_j + b_i) \\ +P(h_i=1|\mathbf{v}) = \sigma(\sum_i w_{ij}v_i + c_j)\end{split}$$ + +where $$\sigma$$ is the logistic sigmoid function: + +$$\sigma(x) = \frac{1}{1 + e^{-x}}$$ + +#### 3.3. Training phase + +The ultimate goal of RBM is to learn feature of the data (representation learning). In order to accomplish that, it uses a loss function to check whether the reconstructed data is close to the training data. If you work with today's neural networks long enough, you may ask "Why shouldn't we use L2, BCE, ...". However, at the time RBM was invented, these terminologies were not popular in the field. However, professor Hinton was inspired from energy in physics and decided to apply it, and that is how RBM was born. + +Basically, energy is a measure of the system's state that indicates how "stable" or "likely" that state is. In an RBM, the energy function assigns a lower energy to states that represent probable or stable configurations, and higher energy to unlikely or unstable configurations. For example, a cool water is stable and has low energy while boilingly hot water is unstable and has much higher energy (it can even power locomotives). In summary, we have to find the parameters that produce lowest energy in the training data and I will explain how it is done below. + +Given a specific configuration of $$v$$ and $$h$$, we map it to the probability space. + +$$p(v,h) = \frac{e^{-E(v,h)}}{Z}$$ + +The $$Z$$ constant is a normalisation factor to ensure that we actually map to the **probability space** (based on Boltzmann distribution). Now let's go to what we're looking for; the probability of a set of visible neurons, in other words, the probability of our data. + +$$p(v)=\sum_{h \in H}p(v,h)=\frac{\sum_{h \in H}e^{-E(v,h)}}{\sum_{v \in V}\sum_{h \in H}e^{-E(v,h)}}$$ + + +To maximise likelihood, for every data point, we have to take a gradient step to make $$p(v) = 1$$. The first thing we do is taking the log of $$p(v)$$. We will be operating in the log probability space from now on in order to make the math feasible. + +$$\log(p(v))=\log[\sum_{h \in H}e^{-E(v,h)}]-\log[\sum_{v \in V}\sum_{h \in H}e^{-E(v,h)}]$$ + +Let's take the gradient with respect to the parameters in $$p(v)$$. + +$$\begin{align} +\frac{\partial \log(p(v))}{\partial \theta}=& +-\frac{1}{\sum_{h' \in H}e^{-E(v,h')}}\sum_{h' \in H}e^{-E(v,h')}\frac{\partial E(v,h')}{\partial \theta}\\ & + \frac{1}{\sum_{v' \in V}\sum_{h' \in H}e^{-E(v',h')}}\sum_{v' \in V}\sum_{h' \in H}e^{-E(v',h')}\frac{\partial E(v,h)}{\partial \theta} +\end{align}$$ + +Now I did this on paper and wrote the semi-final equation down as to not waste a lot of space on this site. I recommend you derive these equations yourself. Now I'll write some equations down that will help out in continuing our derivation. Note that: $$Zp(v,h)=e^{-E(v,h')}$$, $$p(v)=\sum_{h \in H}p(v,h)$$, and that $$p(h \vert v) = \frac{p(v,h)}{p(h)}$$. + +$$\begin{align} +\frac{\partial log(p(v))}{\partial \theta}&= +-\frac{1}{p(v)}\sum_{h' \in H}p(v,h')\frac{\partial E(v,h')}{\partial \theta}+\sum_{v' \in V}\sum_{h' \in H}p(v',h')\frac{\partial E(v',h')}{\partial \theta}\\ +\frac{\partial log(p(v))}{\partial \theta}&= +-\sum_{h' \in H}p(h'|v)\frac{\partial E(v,h')}{\partial \theta}+\sum_{v' \in V}\sum_{h' \in H}p(v',h')\frac{\partial E(v',h')}{\partial \theta} +\end{align}$$ + +After coming to equation (4), we still have a minor problem. Take a closer look at (4), we see that the second term of the equation requires simultaneous sampling of $$v'$$ and $$h'$$. In this scenario, we will use Gibbs sampling to overcome this (Check out a very intuitive explaination at [Gibbs Sampling : Data Science Concepts][Gibss_sampling_video]). + +### 4. Hopfield Network + +#### 4.1. Introduction to Hopfield network + +Hopfield network, similarly, is another physics-inspired invention in this field. It also uses energy as a loss function for optimisation but have some differences in uses and the ways it works. + +Hopfield is sometimes associated with content-addressable memory of human brains as it closely resembles them. For example, we usually have a rather vague image popped inside our head before we can fully retrieve the information in the brain. This evidence indicates that we ask our brain "Do you remember this blurry information and can get me a better, higher-quality version of it ?". And Hopfield network does exactly that, you input a perturbed information and it will return more refined version of the input. + +
+ +
+ +#### 4.2. Inference phase + +
+ +
+ +Updating one unit (node in the graph simulating the artificial neuron) in the Hopfield network is performed using the following rule: + +$$ +s_i \leftarrow +\begin{cases} ++1 & \text{if } \sum_j w_{ij} s_j \geq \theta_i, \\ +-1 & \text{otherwise}. +\end{cases} +$$ + +where: + +* $$w_{ij}:$$ is the strength of connection weight from unit j to unit i (the weight of the connection). + +* $$s_i:$$ is the state of unit i. + +* $$\theta_i:$$ is the threshold of unit i + +Updates in the Hopfield network can be performed in two different ways: + +* **Asynchronous**: Only one unit is updated at a time. This unit can be picked at random, or a pre-defined order can be imposed from the beginning. + +* **Synchronous**: All units are updated at the same time. This requires a central clock +to the system in order to maintain synchronisation. + +#### 4.3. Training phase + +Like RBM, Hopfield network also uses energy for optimisation. + +$$E = -\frac{1}{2} \sum_{i,j}w_{ij}s_is_j - \sum_{i}\theta_i s_i$$ + +Usually, practitioners remove the $\theta$ for less computation by setting it to 0. And the energy becomes: + +$$E = -\frac{1}{2} \sum_{i, j}w_{ij} s_i s_j$$ + +The energy here is the first order function, so taking derivative according to $$w_{ij}$$ to find the optimal point is impractical. Therefore, we have to apply mathematical transformations to figure out. + +$$-\frac{1}{2} \sum_{i, j}w_{ij} s_i s_j \ge -\frac{1}{2} \sum_{i, j} s^2_i s^2_j$$ + +$$<=> w^*_{ij} = \sum_{ij} s^2_i s^2_j \quad \text{(for 1 sample)}$$ + +$$<=> w^*_{ij} = \frac{1}{N} \sum_{n}^{N} \sum_{ij} s^2_i s^2_j \quad \text{for N samples}$$ + +### 5. Conclusion + +In this post, I have walked you through the history and working of the two famous models that have laid the foundation for the rapid advancement of neural nets. As time progresses, better and more complicated models will be released, however, diving into how the basics work always help. + +This is the end to the blog and I wish it could be useful to you. Peace out! + + + + +### References +1\. [A Brain-Inpsired Algorithm For Memory - Youtube][hopfield_video1] +2\. [Hopfield network - Wikipedia][hopfield_net_wiki] +3\. [Neural network models (unsupervised)][rbm_sklearn] +4\. [Intuition Behind Restricted Boltzmann Machine (RBM)][rbm_loss_explanation] + + +[hopfield_video1]: https://www.youtube.com/watch?v=1WPJdAW-sFo&t=1293s +[hopfield_net_wiki]: https://en.wikipedia.org/wiki/Hopfield_network +[rbm_sklearn]: https://scikit-learn.org/1.5/modules/neural_networks_unsupervised.html +[rbm_loss_explanation]: https://datascience.stackexchange.com/questions/15595/intuition-behind-restricted-boltzmann-machine-rbm?fbclid=IwY2xjawGff_RleHRuA2FlbQIxMAABHaGXC2cfR-8YZEermLX7nu5WYvC_T5WweccQJ8xOyRqSztoZ6kolk3gbJQ_aem_l5rKaKIW4qmnEA7wUEB4_A +[Gibss_sampling_video]: https://www.youtube.com/watch?v=7LB1VHp4tLE \ No newline at end of file diff --git a/_posts/2024-11-27-EM.md b/_posts/2024-11-27-EM.md new file mode 100644 index 00000000000..7a3d17332d5 --- /dev/null +++ b/_posts/2024-11-27-EM.md @@ -0,0 +1,115 @@ +--- +title: "Expectation-Maximisation Explained" +mathjax: true +layout: post +categories: media +--- + + +### 1. Problem statement + +Edmond is an intellectually outstanding student. One day, he is tasked with measuring heights of all the students in his school. The task requires filling in 3 fields: name, gender, and height. Unfortunately, he forgets the gender column and worries that the teacher might notice and think poorly of him. Being a smart and trustworthy student, Edmond cannot let that happen. Determined to fix the mistake, he searches for an efficient and accurate way to fill in the missing information. That is how he comes across the concept of "Expectation-Maximisation". + +### 2. Expectation-Maximisation (EM) + +
+ +
+ +In general, the Expectation-Maximisation (EM) algorithm is used to estimate hidden variables or distributions from observed data when some information is incomplete or missing. It iteratively alternates between assigning probabilities to the missing data (Expectation step) and optimizing parameters based on these assignments (Maximisation step). + +In Edmond's case, the observed data consists of the names and heights of the students, while the missing data is the gender. By applying EM, Edmond can i**nfer the gender** for each recorded height based on statistical patterns, such as the distribution of heights typically associated with different genders in the school population. This allows him to fill in the missing fields accurately, even though the gender information was initially incomplete. + +### 3. How it works + +
+ +
+ +Given the *statistical model* which generates a set $$\mathbf{X}$$ of observed data, a set of unobserved latent data or *missing values* $$\mathbf{Z}$$, and a vector of unknown parameters $$\theta$$, along with a *likelihood function* $$L(\theta; \mathbf{X}, \mathbf{Z}) = p(\mathbf{X}, \mathbf{Z} \mid \theta)$$ the *maximum likelihood estimate* (MLE) of the unknown parameters is determined by maximising the *marginal likelihood* of the observed data: + +$$ +L(\theta; \mathbf{X}) = p(\mathbf{X} \mid \theta) = \int p(\mathbf{X}, \mathbf{Z} \mid \theta) p(\mathbf{Z} \mid \theta) \, d\mathbf{Z}. +$$ + +However, this quantity is often intractable since $$\mathbf{Z}$$ is unobserved and the distribution of $$\mathbf{Z}$$ is unknown before attaining $$\theta$$. + +### The EM Algorithm + +The EM algorithm seeks to find the maximum likelihood estimate of the marginal likelihood by iteratively applying these two steps: + +**Expectation step (E step):** Define $$Q(\theta \mid \theta^{(t)})$$ as the *expected value* of the log *likelihood function* of $$\theta$$, with respect to the current *conditional distribution* of $$\mathbf{Z}$$ given $$\mathbf{X}$$ and the current estimates of the parameters $$\theta^{(t)}$$: + +$$ +Q(\theta \mid \theta^{(t)}) = \mathbb{E}_{Z \sim p(\cdot \mid \mathbf{X}, \theta^{(t)})} \left[ \log p(\mathbf{X}, \mathbf{Z} \mid \theta) \right]. +$$ + +**Maximization step (M step):** Find the parameters that maximize this quantity: + +$$ +\theta^{(t+1)} = \underset{\theta}{\operatorname{arg\,max}} \, Q(\theta \mid \theta^{(t)}). +$$ + +More succinctly, we can write it as one equation: + +$$ +\theta^{(t+1)} = \underset{\theta}{\operatorname{arg\,max}} \, \mathbb{E}_{Z \sim p(\cdot \mid \mathbf{X}, \theta^{(t)})} \left[ \log p(\mathbf{X}, \mathbf{Z} \mid \theta) \right]. +$$ + +### 4. An Hands-on example + +One famous algorithm is usually associated with EM is Gaussian Mixture. Basically, Gaussian Mixture is an unsupervised algorithm that is mostly used to cluster data. In this section, in order to give you a better understanding of how EM and Gaussian mixture work, I will use a simple binomial mixture example. + +
+ +
+ +Imagine that you have two coins with unknown probabilities of heads, denoted p and q respectively. The first coin is chosen with probability $$\pi$$ and the second is chosen with probability $$1 - \pi$$. The chosen coin is flipped once and the result is recorded. $$x = \{1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1\}$$ (Heads = 1, Tails = 0). Let $$Z_i \in \{0, 1\}$$ denotes which coin was used on each toss. + +Applying EM to the example, we start by using binary cross entropy (BCE) to determine the similarity between 2 distributions: + +$$Q(\theta \mid \theta^{(t)}) = \mathbb{E} \left[ \sum_{i}^{n}z_i \log(\pi p^{x_i}(1-p)^{1-x_i}) + (1-z_i) \log((1-\pi) q^{x_i} (1-q)^{1-x_i}) \right]$$ + + +$$ += \sum_{i=1}^n \mathbb{E}[z_i \mid x_i, \theta^{(t)}] \left[ \log \pi + x_i \log p + (1 - x_i) \log (1 - p) \right] +$$ + +$$ ++ \left( 1 - \mathbb{E}[z_i \mid x_i, \theta^{(t)}] \right) \left[ \log (1 - \pi) + x_i \log q + (1 - x_i) \log (1 - q) \right] +$$ + +Next, we compute $$\mathbb{E}[z_i \mid x_i, \theta^{(t)}]$$: + +$$ +\mu_i^{(t)} = \mathbb{E}[z_i \mid x_i, \theta^{(t)}] = p(z_i = 1 \mid x_i, \theta^{(t)}) +$$ + +$$ += \frac{p(x_i \mid z_i, \theta^{(t)}) p(z_i = 1 \mid \theta^{(t)})}{p(x_i \mid \theta^{(t)})} +$$ + +$$ += \frac{\pi^{(t)} [p^{(t)}]^{x_i} [(1 - p^{(t)})]^{1-x_i}}{\pi^{(t)} [p^{(t)}]^{x_i} [(1 - p^{(t)})]^{1-x_i} + (1 - \pi^{(t)}) [q^{(t)}]^{x_i} [(1 - q^{(t)})]^{1-x_i}} +$$ + +Maximising $$ Q(\theta \mid \theta^{(t)})$$ with respect to $$\theta$$ yields the update equations: + +$$ +\frac{\partial Q(\theta \mid \theta^{(t)})}{\partial \pi} = 0 \implies \pi^{(t+1)} = \frac{1}{n} \sum_i \mu_i^{(t)} +$$ + +$$ +\frac{\partial Q(\theta \mid \theta^{(t)})}{\partial p} = 0 \implies p^{(t+1)} = \frac{\sum_i \mu_i^{(t)} x_i}{\sum_i \mu_i^{(t)}} +$$ + +$$ +\frac{\partial Q(\theta \mid \theta^{(t)})}{\partial q} = 0 \implies q^{(t+1)} = \frac{\sum_i (1 - \mu_i^{(t)}) x_i}{\sum_i (1 - \mu_i^{(t)})}. +$$ + + +### 5. Conclusion + +EM is an algorithm that helps us determine the hidden distributions of the data in the way we expect. For example, you assume that your coin is tossed by two different coins or two different persons in the aforementioned example and find distributions to determine which one is more likely and which one is less likely for each observation. Similarly, in the case of Edmond, EM can help him distinguish between the heights of male and female students. + +In the blog, I have recapped the motivation behind EM and explained how it works, and I hope it helps. Thanks for reading and see you in the next blog!. \ No newline at end of file diff --git a/_posts/2024-11-29-LoRA.md b/_posts/2024-11-29-LoRA.md new file mode 100644 index 00000000000..5c8f3aab5f6 --- /dev/null +++ b/_posts/2024-11-29-LoRA.md @@ -0,0 +1,35 @@ +--- +title: "LoRA: Low-Rank Adaptation of Large Language Models" +mathjax: true +layout: post +categories: media +--- + +Low-Rank Adaptation (LoRA) was released by Edward Hu and his colleages at Microsoft. The paper was considered game-changing in natural language processing (NLP) field at the time due to its easy implementation and noticeable efficiency. + +### 1. Problem statement + +Nowadays, we rarely train a new model from scratch but to take an available pre-trained model and fine-tune it on our datasets. Despite that fine-tuning is significantly faster and less computationally expensive, the cost is still high, especially when model sizes grow. + +To tackle that, Edward and his team proposed a technique to cleverly reduce computational cost without affecting the output of the fine-tuning process. + +### 2. Authors' solution + +A neural network contains many dense layers which perform matrix multiplication. The weight matrices in these layers typically have full-rank. When adapting to a specific task, Aghajanyan et al. (2020) shows that the **pre-trained language moels have a low "intrinsic dimension" and can still learn efficiently despite a random project to a smaller subspace**. Taking advantage of this conclusion, Edward and his team applied it to the fine-tuning phase and found surprisingly good results. For a pre-trained weight matrix $W_0 \in \mathbf{R}^{d \times k}$, they limit its update by adding a component to weights of a layer. + +$$h = W_0 x + \Delta W x = W_0x + BAx \quad \text{, where } B \in \mathbf{R}^{d \times r}, A \in \mathbf{R}^{r \times k}$$ + +In the formula above, the rank $r \ll min(d, k)$.During training, $W_0$ is frozen and does not receive gradient updates, while $A$ and $B$ contain trainable parameters. + +
+ +
+ +At the start of a fine-tuning scheme, A is sampled from a random Gaussian distribution while B is a zero matrix, so $\Delta W = BA = \mathbf{0}$. Additionally, a constant $\frac{\alpha}{r}$ is introduced to the $\Delta W$. As the constant is not crucial to the fine-tuning, the authors set it to the first rank ($r$) at each experiment (They tried many ranks but kept $\alpha$ fixed at the initial value). + +### 3. Let's build it + +### 4. Conclusion + + + diff --git a/_posts/image-1.png b/_posts/image-1.png new file mode 100644 index 00000000000..ff0b2a028f6 Binary files /dev/null and b/_posts/image-1.png differ diff --git a/_posts/image-2.png b/_posts/image-2.png new file mode 100644 index 00000000000..de1b549682b Binary files /dev/null and b/_posts/image-2.png differ diff --git a/_posts/image-3.png b/_posts/image-3.png new file mode 100644 index 00000000000..c181d639de1 Binary files /dev/null and b/_posts/image-3.png differ diff --git a/_posts/image-4.png b/_posts/image-4.png new file mode 100644 index 00000000000..3462d143db4 Binary files /dev/null and b/_posts/image-4.png differ diff --git a/_posts/image.png b/_posts/image.png new file mode 100644 index 00000000000..3fffd8515bc Binary files /dev/null and b/_posts/image.png differ diff --git a/about_me.md b/about_me.md new file mode 100644 index 00000000000..52d3435c60e --- /dev/null +++ b/about_me.md @@ -0,0 +1,16 @@ +--- +title: "About" +permalink: "/about/" +layout: page +--- + +Hi, mình là Việt. +Mình là cựu sinh viên khóa K18 Kỹ Thuật Điều khiển và Tự Động Hóa, Đại học Bách Khoa TP.HCM. Hiện tại, mình đang làm kỹ sư AI tại công ty Ftech. + +Mục đích của việc viết blog này là để mình có thể chia sẻ kiến thức cũng như lắng nghe phản hồi từ người đọc, vì vậy nếu thấy sai sót ở đâu thì mong mọi người đừng ngần ngại gửi email cho mình với nha. + +Dưới đây là những lĩnh vực yêu thích của mình: +* Trí tuệ nhân tạo (Artificial Intelligence) +* Học máy (Machine Learning) +* Cấu trúc dữ liệu & giải thuật +