Thứ Sáu, 28 tháng 12, 2018

Facade Pattern

Đây là 1 pattern rất dễ, cái khó nhất có lẽ là đọc cho đúng tên pattern. =)) Facade không đọc là "pha cây" , đọc đúng phải là "phờ sát" (fəˈsɑːd).
oke. Vậy pattern này là gì. Đơn giản lắm! Bạn thử tưởng tượng trong quá trình bạn code, bạn sẽ phân chia các class khác nhau. Theo thời gian thì sẽ có rất nhiều class khác nhau, và các class này sẽ tương tác qua lại với nhau. Điều này thật sự rất rối rắm.
Và để giảm bớt sự rối rắm này, Facade pattern được áp dụng vào. Nó sẽ tạo ra 1 lớp Facade bao bọc bên ngoài, và xử lý những kết nối rối rắm dùng chúng ta.
Facade Pattern: tạo ra 1 giao diện cao hơn giúp người dùng dễ dàng sử dụng các giao diện thấp
Okie. Bây giờ ta vào ví dụ và code nào. Giả sử ta đang thiết kế 1 chiếc xe hơi chẳng hạn. Chiếc xe hơi thì có thể rẻ phải rẻ trái. Vậy lúc rẻ thì nó sẽ làm gì?
Đầu tiên nó sẽ nhả chân côn để mô tơ của xe chạy chậm lại tránh cho xe bị lật khi qua. Sau đó nó sẽ xoay vô lăng để cho bánh xe rẻ qua 1 bên. Đếm sơ sơ qua là ta có 4 class rồi đấy.
public class Wheel {   
   public void turnLeft() {
      // re trai
   }
   public void turnRight() {
      // re phai
   }
}
public class Steering {  
   private Wheel wheel = new Wheel(); 
   // xoay vo lang nguoc kim dong ho
   public void rotateLeft() {
      this.wheel.turnLeft();
   }
   // xoay vo lang cùng chieu kim dong ho
   public void rotateRight() {
      this.wheel.turnRight();
   }
}
public class Motor{   
   public void speedUp() {
      // tang toc
   }
   public void slowDown() {
      // giam toc
   }
}
public class Pedal {  
   private Motor motor = new Motor();    
   public void pressDown() {
      this.motor.speedUp();
   }
   public void release() {
      this.motor.slowDown();
   }
}
Thay vì mỗi lần rẻ trái, chúng ta phải xử lý rối rắm các class trên thì thay vào đó, ta sẽ tạo 1 cái facade Car cho dễ xài.
public class Car{   
   private Steering steering = new Steering();
   private Pedal pedal = new Pedal();
   public void turnLeft() {
      this.steering.rotateLeft();
      this.pedal.release();
   }
   public void turnRight() {
      this.steering.rotateRight();
      this.pedal.release();
   }
}
Giờ muốn xe rẻ phải thì chỉ cần.
Car car = new Car();
car.turnRight();
Share:

Adapter Pattern

Mấy năm gần đây, Apple loại bỏ jack tai nghe rồi, chắc mọi người cũng biết. Vậy mọi người có biết nếu ko có jack tai nghe, thì làm sao để nghe được bằng tai nghe có dây bình thường không? Rất đơn giản, người ta chế ra 1 cục chuyển đổi từ đầu cắm jack tai nghe thành đầu sạc iphone, và cái cục này có thể gọi nó là Adapter.
Trong design pattern, chúng ta cũng có 1 khái niệm tương tự. Chúng ta có 1 đoạn class A, muốn xài 1 method x của class B. Nhưng vì lí do nào đó, A không thể xài B được.
Thì chúng ta sẽ tạo ra 1 adapter trung gian là class C. Và nhờ vào class C, A có thể xài được B.
Adapter Pattern: cho ta 1 lớp có thể xài 1 class khác dựa vào interface của nó
Vấn đề ở đây là, tại sao lại có chuyện A không thể xài B được? Chỉ cần khởi tạo B trong A và xài thôi, có gì khó khăn đâu?
Nhưng nếu bạn làm dự án thực tế, bạn sẽ gặp phải những trường hợp mà bạn ko có quyền sửa code. Tức là project của bạn có xài class A, nhưng bạn không có quyền sửa code của A. Lí do là vì, A có thể là thư viện có sẵn, hoặc A có thể là code của một nhóm khác lập trình....
Pattern này quá đơn giản, nên mình sẽ đi vào code luôn.
public class A {
   private ICanUse canUseInterface;
   public A (ICanUse canUseInterface) {
      this.canUseInterface = canUseInterface;
   }
   private void doSomething() {
      this.canUseInterface.request();
   } 
}
Đầu tiên, ta thấy ở trên, ta có class A đã được lập trình sẵn là xài hàm request của ICanUse. Nhưng bây giờ ta lại muốn A xài được hàm specificRequest của interface B dưới đây.
public Interface B {   
   public void specificRequest();
}
public class randomB implements B {
   public void specificRequest() {
      //do something 
   }
}
Để làm được điều này, ta tạo 1 cái Adapter
public class Adapter implements ICanUse {   
   private B b;
   public A (B b) {
      this.b = b;
   }
   public void request() {
       b.specificRequest();
   }
}
Vậy thôi, giờ muốn A có thể xài được specificRequest của B thì chỉ cần.
A a = new A(new Adapter(new B()));
a.request();
Share:

Command Pattern

Thế giới đã đi vào giai đoạn công nghệ bốn chấm không, mọi thứ đều là công nghệ, áp dụng công nghệ vào tất cả mọi thứ trong cuộc sống. Và hiển nhiên, IoT , internet of thing là một xu hướng tất yếu. Và bạn chuẩn bị khởi nghiệp với công ty iot, sản xuất thiết bị thông minh điều khiển mọi thứ trong nhà. Ồ, nghe xịn sò nhể. Cơ mà đầu tiên phải thử làm một sản phẩm đề mô nhỏ nhỏ trước xem có khả thi không đã nào.
Thử tưởng tượng xem, nếu mình dấn thân vào lĩnh vực Smart Home, kết nối tất cả thiết bị ở trong nhà thì nên bắt đầu như nào nhỉ? À đúng rồi, trước hết phải có 1 cái remote dùng để điều khiển. Remote này có thể điều khiển bóng đèn tắt mở nên nó sẽ có 2 cái nút, 1 nút để tắt, 1 nút để mở.
Nhưng trong nhà thì có nhiều loại bóng đèn lắm, phòng khách có đèn chùm, nhà vệ sinh có đèn huỳnh quang, phòng ăn có đèn sợi tóc... cả tá đèn thì lập trình thế quái nào được đây.
A, đúng rồi. Học strategy pattern rồi, áp dụng vào thôi! Đơn giản quá đi mà.
Mình sẽ tạo 1 interface là ILight, interface này có 2 chức năng là on và off. Từ đó có thể tạo bao nhiêu hiện thực của nó tùy thích.
Bây giờ rất đơn giản, chỉ cần ghán 1 nút của remote chạy hàm on, 1 nút chạy hàm off là được. Muốn điều khiển bóng đèn nào thì chỉ cần set hiện thực của nó vào remote là được. Đơn giản.
Nhưng, ngoài bóng đèn ra còn có nhiều cái khác nữa mà. Ví dụ muốn điều khiển cả tivi nữa. Bấm 1 nút là chuyển kênh, bấm 1 nút là tăng âm lượng. Hừm, ok. Mình sẽ áp dụng strategy pattern tương tự cho tivi để phù hợp với các hãng tivi khác nhau như samsung, sony, lg.... Sau đó gia công thêm vài nút lên cái remote, rồi cài đặt cho những nút đó là xong.
Nhưng trong nhà lại có thêm máy giặt, tủ lạnh, quạt điện.... với biết bao nhiêu là chức năng cần thực hiện. Mà mỗi vật như thế thì có biết bao nhiêu là loại khác nhau. Giờ làm sao đây nhỉ? Nếu cứ thiết kế theo kiểu trên chắc người ta cười vào mặt công ty mình mất.
Và đây chính là lúc, chúng ta nên áp dụng Command Pattern.
Command Pattern: đóng gói mỗi yêu cầu thành những đối tượng để bạn có thể tương tác với những đối tượng bằng những yêu cầu khác nhau. Thể loại Pattern này cũng cho phép ta thực hiện được cái hành động undo nữa
Okie! như thường lệ, ví dụ sẽ làm sáng tỏ định nghĩa.
Với ví dụ smart home ở trên, thay vì ta gộp từng loại thiết bị theo strategy pattern, chúng ta sẽ phân chia mỗi yêu cầu thành từng đối tượng. Có nghĩa rằng, với yêu cầu bật đèn - nó sẽ là 1 đối tượng. Yêu cầu tắt đèn - nó cũng sẽ là 1 đối tượng. Yêu cầu chuyển kênh, tăng âm lượng, giặt áo quần, sấy áo quần, bật quạt, tắt quạt.... - mỗi yêu cầu như thế sẽ là 1 đối tượng.
Và bây giờ, những nút bấm của remote không cần phải gán từng chức năng riêng lẽ như on off nữa, mà nó sẽ được gắn vào những đối tượng đã nêu trên. Ví dụ, nếu gọi cái remote là invoker, các thiết bị của chúng ta là receiver, chúng ta sẽ có mô hình Command pattern sau đây.
Quá trình hoạt động sẽ như sau. Đầu tiên ta sẽ set nút bấm trên remote sẽ làm command nào. Sau đó, khi ta bấm nút, class command sẽ trigger hoạt động execute để gọi tới action của thiết bị. Vậy thôi.
Để rõ hơn, ta đi vào vài dòng code nhé. Đầu tiên là ta có các thiết bị.
public interface Light {
   public void turnOn();
   public void turnOff();
}
public interface Tivi{
   public void nextChannel();
}
public class PhilipLight implements Light{
   public void turnOn() {
       // bat bong den
   }   
   public void turnOff() {
       // tat bong den
   }
}
public class SamsungTivi implements Tivi{
   public void nextChannel() {
       // chuyen kenh
   }   
}
Tiếp đến ta tạo interface cho ICommand và các hiện thực của nó.
public interface ICommand {
   public void execute();
   public void undo();
}
public class TurnLightOnCommand implements ICommand {
   private Light lightl
   public void TurnLightOnCommand (Light light) {
       this.light = light;
   }
   public void execute() {
       this.light.turnOn();
   }
   public void undo() {
      // redo turn on.
   }
}
public class SwitchChannelCommand implements ICommand {
   private Tivi tivi
   public void TurnLightOnCommand (Tivi tivi) {
       this.tivi = tivi;
   }
   public void execute() {
       this.tivi.nextChannel();
   }
   public void undo() {
      // redo next channel.
   }
}
Tiếp đến bạn tạo ra invoker cho cái điều khiển.
public class Invoker {
   private ICommand command;
   public void setCommand(ICommand command) {
       this.command = command;
   }
   public void press() {
      this.command.excute();
   }
}
Bây giờ giả sử bạn muốn nút bấm trên remote của bạn dùng để bật bóng đèn Philip ngoài phòng khách chẳng hạn. Bạn chỉ cần:
ICommand command = new TurnLightOnCommand(new PhilipLight());
Invoker invoker = new Invoker();
invoker.setCommand(command);
Và khi bạn ấn nút thì chạy dòng code sau.
invoker.press();
Vậy là xong. Đó chính là Command Pattern. Còn về phần mà undo tác vụ ấy. Chúng ta hãy liên tưởng tới các phần mềm kiểu như photoshop, khi chúng ta thực hiện thao tác nào đó, thì hãy xem mỗi thao tác đó là 1 command. Ví dụ ta vẽ 1 đường thẳng, chính là 1 command, chúng ta đổi màu cũng là 1 command.... Để tổ chức các command đó thì chúng ta có thể dùng stack để lưu, và khi ta ctrl + Z, thì chỉ cần bóc cái command trong stack ra và chạy hàm undo là được. Rất đơn giản.
Share:

Singleton Pattern

Singleton Patern, một pattern supper thú vị sắp được giới thiệu với bạn đây. Tại sao lại thú vị ư? Để mình nói cho bạn nghe nhé: singleton pattern có hàm constructor là private. Nhớ nhé, private đấy! Không phải public đâu. Vậy tại sao lại là private, chúng ta phải đến với tình huống cụ thể mới biết hiểu được nhé.
Okie, giả dụ bạn là một nhà lập trình trang web gạ "cờ hịch" online. Những người nào muốn được cờ hịch free thì sẽ lên trang web của bạn tìm đối tượng. Vậy đầu tiên chúng ta sẽ phải có 1 cái gì nhỉ? À, chúng ta phải có 1 cái room chát tổng, để cho những con trống vào gạ gẫm những con mái, hoặc con trống gạ con trống ._.. Nói chung là ko quan trọng, quan trọng là chúng ta phải có 1 cái room chat tổng để tất cả mọi người cùng vào. Và trang web của chúng ta có rất nhiều theme nhé, có theme hồng cánh sen cho mấy chị bánh bèo, có theme đen mạnh mẽ cho mấy anh 6 múi.... Nhưng dù ở theme nào đi nữa, chúng ta cũng chỉ sử dụng chung 1 cái room chat duy nhất mà thôi, đó là room chat tổng.
Vậy làm sao để từ 1 class room chat, chúng ta chỉ có thể tạo 1 đối tượng room chat duy nhất, mà không thể tạo ra 1 cái room chat thứ 2. Nghe có vẻ khoai đúng ko? Nếu đã là 1 lớp, thì chúng ta làm quái nào mà ngăn người ta tạo ra nhiều hơn 1 thực thể được. Và đây chính là lúc Singleton Pattern được sử dụng.
Singleton Pattern: đảm bảo rằng 1 lớp chỉ có 1 thực thể duy nhất và cho phép ta truy cập ở cấp độ toàn cục
Tức là sao? Tức là: chúng ta có 1 class A nào đó chẳng hạn, thì nếu thiết kế theo singleton pattern, chúng ta không thể nào tạo ra được 2 thực thể khác nhau của A. Nghe có vẻ lạ đúng không, nhưng điều này là hoàn toàn có thể. Bằng cách nào? Haha, đây là lúc bạn nhìn thấy private constructor. Nghe thôi đã thấy hài vl =)) Cấu trúc của 1 singleton pattern.
1 class A thiết kế theo của singleton pattern, sẽ có 1 biến static có kiểu A, 1 hàm constructor private, và 1 hàm createSingleton static. Vì constructor là private, nên chúng ta không thể tạo thực thể cho class bằng lệnh new A(); được, mà thay vào đó, chúng ta sẽ phải dùng hàm createSingleton để khởi tạo 1 thực thể. Và tất nhiên, trong hàm createSingleton này, chúng ta sẽ trả về 1 biến static duy nhất. Điều này ngăn cản việc khởi tạo 2 thực thể khác nhau của 1 lớp.
OK! Vào code để xem thằng singleton này nó được code như nào xem nào. Đầu tiên là khởi tạo 1 lớp ChatRoom, với thiết kế của singleton như trên.
public class ChatRoom {
   private static ChatRoom chatRoom;
   private ChatRoom() {
   }
   public static ChatRoom createChatRoom(){
   
   }
}
Bây h ta chỉ cần viết logic cho createChatRoom là xong.
public class ChatRoom {
   private static ChatRoom chatRoom;
   private ChatRoom() {
   }
   public static ChatRoom createChatRoom(){
       if(chatRoom == null) {
           chatRoom = new ChatRoom();
       }
       return chatRoom;
   }
}
Vậy là xong. Nếu bạn muốn khởi tạo 1 ChatRoom thì chỉ cần.
ChatRoom chatRoom = ChatRoom.createChatRoom();
Lần thứ 2 bạn khởi tạo ChatRoom bằng câu lệnh trên, thì nó vẫn trả về cho bạn đúng 1 thực thể duy nhất. Và đó chính là Singleton Pattern. Chúc trang web gạ cờ hịch của bạn không bị công an bắt và kiếm được nhiều tiền nhé!
Share:

Thứ Năm, 27 tháng 12, 2018

Abstract Factory Pattern

Nếu bạn đã được đọc về Factory Method Pattern, thì mình nghĩ Abstract Factory Pattern chỉ là 1 cái trường hợp đặc biệt của Factory Method Pattern mà thôi.
Abstract Factory Pattern: cung cấp 1 interface cho phép người dùng tạo ra 1 nhóm các đối tượng độc lập hoặc có liên hệ với nhau mà không cần quan tâm tới lớp thực thi của chúng
Nghe có vẻ bù cả đầu đúng không? Nhưng mà sự thật thì cực kì đơn giản.
Quay lại với ví dụ trong bài Factory Method Pattern, chúng ta có cái game phi thuyền bắn súng đấy. Bạn để ý nhé, trong game lúc nào cũng có background (tức là cái hình nền ấy) nó xuất hiện khác nhau với mỗi level. Ví dụ với level 1, mục tiêu là bắn các mảnh thiên thạch thì background sẽ là bầu trời đêm chẳng hạn. Level 2 bắn phá các phi thuyền thì background sẽ là các linh kiện máy móc... vân vân và mây mây. Và tất nhiên, không phải background của chúng ta là random được, nó phải có lý với bối cảnh của enemy. Không phải tự dưng enemy là các mảnh thiên thạch, mà background lại là đồng cỏ được, nó không có phù hợp gì hết. Hay là enemy là mấy con bò, mà background lại là trên dải ngân hà, đúng lafk hông hợp lý. Vậy nên, ứng với mỗi enemy, chúng ta phải có 1 background đi kèm. Vậy để làm được thế thì ta không thể tạo 2 cái Factory riêng biệt cho enemy và background được. Vì có thể, trong tương lai, không những chỉ có background mà có thể có các hiệu ứng mây mưa, sương khói các thứ cũng phải làm sao cho hợp lý. Như vậy sẽ dẫn tới chúng ta có vô vàn Factory riêng lẻ. Và Abstract Factory Pattern lúc này mới được đưa vào sử dụng.
Cùng xem lại mô hình củ Factory Method Pattern nhé.
Ta thấy, mỗi factory sẽ tạo cho chúng ta 1 đối tượng là IProduct. Nhưng ở bài toán hiện tại, chúng ta không chỉ có 1 product (là enemy) nữa, mà chúng ta có đến 2, hoặc 3 đối tượng cơ. Vậy nên, Abstract Factory Pattern cải tiến lại, giúp cho Factory có thể cho ra được 2 nhiều hơn 1 product.
Ví dụ ta có 2 interface IProductEnemy và IProductBackground, mô hình cảu Abstract Factory Pattern sẽ như sau:
Ta thấy, bây h factory của chúng ta đã có thể cho ra 2 product. Và tất nhiên, khi hiện thực IFactory, thì chúng ta cũng đã định nghĩa được những background nào nên đi kèm với enemy nào rồi, nên sẽ ko có hiện tượng bất hợp lý ở UI khi người ta chơi game cả. Vậy thôi, đó là Abstract Factory Pattern.
Bây h thử code vài dòng để hiểu hơn nó là như thế nào nà. Đầu tiên vẫn như cũ, ta tạo interface của enemy và các hiện thực của nó.
public interface Enemy{
    public void speed();
    public void healthy();
} 
public interface FirstLevelEnemy extends Enemy{
} 
public interface SecondLevelEnemy extends Enemy{
}
public class BigMeteorite implements FirstLevelEmemy {
   public void speed() {
       //lam cai gi do
   }
    public void healthy(){
       //lam cai gi do
   }
}
public class VietNamSpaceShip implements SecondLevelEnemy {
   public void speed() {
       //lam cai gi do
   }
    public void healthy(){
       //lam cai gi do
   }
}
Bây giờ chúng ta tạo ra interface cho background và các hiện thực của nó.
public interface Background{
    public void display();
} 
public interface FirstLevelBackground extends Background{
} 
public interface SecondLevelBackground extends Background{
}
public class DarkGalaxy implements FirstLevelBackground {
   public void display() {
       //lam cai gi do
   }   
}
public class OldShipFLoor implements SecondLevelBackground {
   public void display() {
       //lam cai gi do
   } 
}
Tiếp đến ta tạo ra interface Factory và các hiện thực của nó.
public interface IFactory{
    public Enemy createEnemy();
    public Background createBackground();
} 
public class FirstLevelFactory implements IFactory {
    public Enemy createEnemy() {
       return RandomEnemy();
    }
    public Background createBackground() {
       return RandomEnemy();
    }  
    private FirstLevelEnemy RandomEnemy() {
       //lam gi do de random cac kieu
    }
    private FirstLevelBackground RandomBackground() {
       //lam gi do de random cac kieu
    }
}
public class SecondLevelFactory implements IFactory {
    public Enemy createEnemy() {
       return RandomEnemy();
    } 
    public Background createBackground() {
       return RandomEnemy();
    }  
    private SecondLevelEnemy RandomEnemy() {
       //lam gi do de random cac kieu
    }
    private SecondLevelBackground RandomBackground() {
       //lam gi do de random cac kieu
    }
}
Và cuối cùng là thêm 1 lớp factory bên ngoài IFactory được nữa. Ví dụ:
public class MotherOfFactory {
     public static IFactory createFactory(int level) {
         switch (level) {
             case 1: return FirstLevelFactory(); break;  
             case 2: return SecondLevelFactory(); break;  
             //and so on 
         }
     }
}
Bây giờ giả sử bạn ở level 1, thì có thể khởi tạo enemy bằng cách.
Enemy enemy = MotherOfFactory.createFactory(1).createEnemy();
Nếu bây giờ, bạn muốn lấy background, thì chỉ cần:
Background background = MotherOfFactory.createFactory(1).createBackground();
Lúc này Background và Enemy của bạn sẽ rất hợp lý, vì bạn đã định nghĩa nó ngay trong hiện thực của IFactory rồi.
Vậy thôi, quá đơn giản phải không?
Share:

Factory Method Pattern

Bao nhiêu bạn đã từng chơi game phi thuyền bắn súng? Giơ tay lên đi đừng ngại ngần. Có bạn nào chưa chơi game phi thuyền bắn súng không? Nếu chưa chơi thì nói thật, tuổi thơ bạn bất hạnh ghê gớm. Vì sao? vì trò chơi này là cả một tuổi thơ dữ dội, nó là cả 1 tinh hoa của nghệ thuật bắn súng trong đó. Vậy nên, chưa chơi thì hãy đi chơi đi, chơi ngay đi, vì trong bài này, mình sẽ lấy game đó làm ví dụ đấy. =))
Okie, trờ lại với bài này. Nếu như các bạn chơi game phi thuyền bắn súng thì các bạn sẽ thấy, trò chơi có các mảnh thiên thạch to nhỏ khác nhau, xuất hiện liên tục, nhiệm vụ của bạn là lo bắn phá hết tất cả cái đống đó. Vậy để generate ra được cái đống thiên thạch đó thì như nào. I zì, bạn dùng strategy pattern như trong đã học ở bài này rồi thiết kế ra thôi là được.
Giả sử bạn có thiên thạch to, thiên thạch nhỏ, thiên thạch vừa. Mỗi thiên thạch có tốc độ bay khác nhau, có lượng máu khác nhau, thì các bạn có thể dễ cmn dàng áp dụng strategy pattern để thiết kế.
Tất nhiên, bạn có thể tạo interface cho speed và healthy, nhưng ở đây mình tối giản nó lại, vì chúng ta đang học factory chứ ko phải strategy. Okie! Nói chung bạn cứ tạm hiểu là cái chúng ta đang thiết kế mấy viên thiên thạch rất hợp lý vào logic rồi.
Tiếp! Bây giờ làm thế nào để ta tạo ra mấy viên thiên thạch này bay lung tung xì phèo như game đây? Không lẽ cứ khởi tạo từng viên thiên thạch rồi ném nó vào UI à? Nghe như cứt ấy nhỉ? Vậy nên, người ta mới tạo ra ý tưởng là: "À, mình phải tạo 1 lớp, có nhiệm vụ sản sinh ra mấy cái IEnemy. IEnemy này có thể là thiên thạch to, thiên thạch nhỏ, thiên thạch vừa. Chỉ cần ta truyền parameter vào cho nó là nó sẽ tự động trả về cho mình 1 viên thiên thạch." . Ố dè, và đây chính là Factory Pattern.
Có thể hiểu ở đây, Factory là 1 cái nhà máy dùng để sản xuất IEnemy, dựa vào tên của enemy bạn muốn tạo. Và cái thể loại này ấy, người ta gọi là Simple Factory Pattern. Nhiều người cho rằng đây ko phải là pattern, ok thôi, vì mình là thằng ko thích định nghĩa nên người ta muốn bàn gì thì bàn, quan trọng là mình hiểu nó là cái gì là được rồi.
Ok! nhìn chung có vẻ ổn đúng không? Vậy tại sao người ta lại sản sinh ra thêm cái Factory Method Pattern làm gì? Sao không xài Simple Factory Pattern cho rồi, nhìn cũng ngon mà. Hờ, vấn đề là ở đây: chả có cái game củ chuối nào mà cứ bắn hoài mấy cái thiên thạch từ đầu tới cuối cả. Bạn chơi 1 hồi, tất nhiên độ khó của game sẽ tăng dần, mà độ khó game tăng tức là enemy cũng phải tăng dần. Ví dụ bạn chơi được 100 điểm, game sẽ chuyển qua level 2, chơi được 200 điểm game sẽ chuyển qua level 3. Và khi qua level 2 , level 3, game sẽ không còn là bắn thiên thạch nữa, mà chúng ta sẽ phải bắn tàu chiến, bắn rocket chẳng hạn. Vậy để giải quyết cái này thì làm sao?
Hừm, có thể chúng ta sẽ tạo ra những cái factory mới cho từng level tương ứng. Ví dụ ta có: Factory1 để sản sinh ra enemy của level 1, Factory2 để sản inh ra enemy của level 2... Nhưng nếu có 1000 level thì sao? Có thể nhiều bạn bảo: "Làm cứt gì có game 1000 level". Ôi con xin lạy, ý con chỉ là muốn lấy ví dụ để cho thấy cái vấn đề gặp phải của chúng ta thôi. Là bây giờ chúng ta sẽ có rất nhiều factory, và để đưa ra được enemy cho người chơi bắn, chúng ta phải xử lý để tiếp tục chọn factory tương ứng với level. Như vậy thì không hay. Giờ phải nghĩ ra cách mới thôi... Làm sao nhỉ???
À, đúng rồi!!! Chúng ta sẽ tạo ra 1 cái interface tên là IFactory, và những factory của từng level sẽ là hiện thực của IFactory này. Chúng sẽ có chung cơ chế sản sinh enemy là createEnemy, nhưng sẽ sản sinh ra những nhóm enemy khác nhau. Đấy, bạn bây giờ đã sử dụng Factory Method Pattern rồi đấy.
Factory Method Pattern: định nghĩa ra 1 cái interface cho phép ta khởi tạo 1 đối tượng, NHƯNG! Nó sẽ để cho lớp con của nó quyết định việc tạo ra đối tượng cụ thể. Factory Method Pattern nhường lại quyền khởi tạo cho lớp con của nó
Vậy cấu trúc cơ bản của 1 Factory Method Pattern nó là như nào? Đầu tiên ta sẽ có 1 cái interface product, và những hiện thực của nó.
Sau đó ta có 1 cái interface của Factory và những hiện thực con của nó.
Vậy đấy, Factory Method Pattern là vậy. Okie, chúng ta sẽ thử code 1 cách ngắn gọn xem nó hoạt động như nào nhá.
Đầu tiên là interface Enemy và 2 hiện thực con ở 2 level khác nhau. Chúng ta sẽ phân tầng nó để nhìn code có vẻ đẹp hơn.
public interface Enemy{
    public void speed();
    public void healthy();
} 
public interface FirstLevelEnemy extends Enemy{
} 
public interface SecondLevelEnemy extends Enemy{
}
public class BigMeteorite implements FirstLevelEmemy {
   public void speed() {
       //lam cai gi do
   }
    public void healthy(){
       //lam cai gi do
   }
}
public class VietNamSpaceShip implements SecondLevelEnemy {
   public void speed() {
       //lam cai gi do
   }
    public void healthy(){
       //lam cai gi do
   }
}
Tiếp đến ta tạo ra interface Factory và các hiện thực của nó.
public interface IFactory{
    public Enemy createEnemy();
} 
public class FirstLevelFactory implements IFactory {
    public Enemy createEnemy() {
       return RandomEnemy();
    } 
    private FirstLevelEnemy RandomEnemy() {
       //lam gi do de random cac kieu
    }
}
public class SecondLevelFactory implements IFactory {
    public Enemy createEnemy() {
       return RandomEnemy();
    } 
    private SecondLevelEnemy RandomEnemy() {
       //lam gi do de random cac kieu
    }
}
Bạn có thể bọc thêm 1 lớp factory bên ngoài IFactory được nữa. Ví dụ:
public class MotherOfFactory {
     public static IFactory createFactory(int level) {
         switch (level) {
             case 1: return FirstLevelFactory(); break;  
             case 2: return SecondLevelFactory(); break;  
             //and so on 
         }
     }
}
H bạn muốn tạo enemy của level 1 thì chỉ cần gõ vài dòng.
Enemy enemy = MotherOfFactory.createFactory(1).createEnemy();
H bạn muốn tạo enemy của level 2 thì chỉ cần gõ:
Enemy enemy = MotherOfFactory.createFactory(2).createEnemy();
Share:

Thứ Tư, 26 tháng 12, 2018

Decorator Pattern

Decorator Pattern là gì? Hừm, mình sẽ cho bạn định nghĩa dễ hiểu trước. Giả sử bạn có 1 đối tượng, và bạn muốn đối tượng đó thực hiện 1 tác vụ nào đó.
Nhưng đôi lúc, bạn lại muốn respond của bạn là 1 cái gì đó khác hơn, 1 cái gì đó được thêm mắm thêm muối vào cho mặn mà hơn, thì bạn phải làm thế nào? À, đúng rồi! Bạn sẽ thay đổi cách mà đối tượng respond bằng cách sửa code của đối tượng. Nhưng sau một thời gian, bạn lại muốn respond khác hơn, 1 chút cay nồng hơn, thì bạn phải làm sao? À, bạn lại thay đổi code của đối tượng tiếp. Và bạn chợt nhận ra, sao cái cách này nó lìn lìn thế nào ấy nhỉ? Đúng rồi đấy, vậy nên người ta mới nghĩ ra 1 cái pattern gọi là decorator patter. Cái loại này sẽ tạo ra 1 cái decorator bao bọc cái đối tượng ban đầu, sau đó nói lấy cái respond của cái đối tượng đó rắc thêm chút mắm muối rồi mới trả ngược lại cho bạn.
Như trong hình trên, cái decorator sẽ lấy respond từ object, sau đó nó sẽ rắc thêm chút muối, tẩm thêm 1 chút mắm rồi mới trả về cho bạn. Nếu như bạn muốn thêm tí cay nồng ư? Dễ thôi!
Chỉ cần thêm 1 cái decorator bao bọc bên ngoài, rồi đổ thêm tí ớt là xong. Phương pháp bao bọc thêm thắt này người ta gọi là decorator pattern.
Decorator Pattern: gắn thêm 1 vài tính năng cho 1 đối tượng 1 cách linh động. Loại pattern này cho phép ta mở rộng lớp con 1 cách linh động hơn
Arggg, sao mình ghét mấy cái khái niệm khô khan này thế nhỉ? hừm, mình sẽ bay vào ví dụ cho các bạn hiểu ngay nà. Có bạn nào đi uống trà sữa chưa? Ai nói chưa thì đi mua 1 ly đi rồi về đọc tiếp nhá. Khi bạn vào 1 quán chà sữa, bạn order sẽ gọi loại trà sữa đầu tiên. Ví dụ bạn gọi chà sữa nhà làm, hoặc có bạn thì gọi chà sữa đài loan. Sau khi bạn order loại trà sữa xong, mấy bé thu ngân sẽ kiểu gạ gẫm:
- ahihi, anh có muốn dùng thêm topping ko ạ?
- có những loại topping gì em?
- ahihi, có nhiều loại topping lắm, anh cứ đọc ở menu ạ.
- vậy lấy anh thêm chân châu chắng và hạt thủy tinh nha
- ahihi, ok anh.
- ahihi, dm em. làm nhanh anh còn về đọc design pattern=))
Đó là 1 quá trình order trà sữa điển hình. Và khi tầm tờ hóa đơn ấy, bạn sẽ đọc được dòng chữ kiểu: nhà làm + trân châu trắng. Cũng được thôi, nhưng mình là dân code đẳng cấp mà, mình muốn cái gì đó kiểu xịn sò hơn, kiểu: "Trà sữa nhà làm và trân châu trắng và hạt thủy tinh". Đấy, như thế mới gọi là bill đẳng cấp. Nhưng làm thể nào để code generate ra được dòng miêu tả xịn như thế được. Đó là lúc ta áp dụng Decorator Pattern.
Đầu tiên ta có 1 cái Interface là Milk Tea bao gồm 2 phương thức là getDescription() và getCost() chẳng hạn.
Tiếp đến ta tạo 2 cái hiện thực cho interface này là HomeMade và Taiwain.
Giờ ta tạo 1 cái interface Topping, nhưng interface này sẽ là kế thừa interface milk tea. Đồng thời tạo 2 cái hiện thực tương ứng là WhiteBubble và GlassesBubble.
Thiết kế sẽ là như thế. Nhưng phải đi vào code, bạn mới hiểu được decorator ở đây là gì. Đầu tiên là interface milk tea và 2 hiện thực homemade, taiwan.
public interface IMilkTea{
    public String getDes();
    public int getCost();
} 

public class HomeMade implements IMilkTea {
    public String getDes() {
        return "tra sua nha lam";
    }
    public int getCost() {
        return 10000;
    }
}

public class Taiwan implements IMilkTea {
    public String getDes() {
        return "tra sua dai loan";
    }
    public int getCost() {
        return 15000;
    }
}
Tiếp đến là interface topping và các hiện thực tương ứng.
public interface Topping extends IMilkTea {
} 

public class WhiteBubble implements Topping {
    private IMilkTea iMilkTea;
    public WhiteBubble (IMilkTea iMilkTea) {
         this.iMilkTea = iMilkTea;
    }
    public String getDes() {
        return iMilkTea.getDes() + " va tran chau trang";
    }
    public int getCost() {
        return iMilkTea.getCost() + 5000;
    }
}

public class GlassesBubble implements Topping {
    private IMilkTea iMilkTea;
    public GlassesBubble (IMilkTea iMilkTea) {
         this.iMilkTea = iMilkTea;
    }
    public String getDes() {
        return iMilkTea.getDes() + " va hat thuy tinh ";
    }
    public int getCost() {
        return iMilkTea.getCost() + 7000;
    }
}
Vậy là xong, bây giờ giả sử khách hàng muốn mua 1 ly trà sữa nhà làm, ta sẽ có đối tượng trà sữa nhà làm và in ra hóa đơn.
HomeMade homeMade = new HomeMade();
System.out.println(homeMade.getDes());
System.out.println(homeMade.getCost());
Nếu khách hàng muốn có topping trân châu trắng, có luôn cho khách.
WhiteBubble whiteBubble = new WhiteBubble(new HomeMade());
System.out.println(whiteBubble.getDes());
System.out.println(whiteBubble.getCost());
Khách đổi ý muốn thêm cả hạt thủy tinh nữa, chơi luôn cho khách.
GlassesBubble glassesBubble = new GlassesBubble (new WhiteBubble(new HomeMade()));
System.out.println(glassesBubble.getDes());
System.out.println(glassesBubble.getCost());
Vậy thôi. Đó chính là Decorator Pattern. Muốn thêm mắm thêm muối gì thì cứ thêm, ko phải sợ. Hê hê
Share:

Thứ Ba, 25 tháng 12, 2018

Observer Pattern

Với loại Pattern này, mình sẽ đưa ra tình huống trước. Ví dụ bạn đang làm 1 cái web dự báo thời tiết chẳng hạn. Trang web của bạn bắt buộc phải có thông tin thời tiết rồi đúng ko? Vậy là ta có 1 đối tượng gọi là "Weather". Đối tượng Weather này có 1 biến tên là "Temperature", giá trị biến này sẽ có sự thay đổi theo thòi gian. Ví dụ đêm lạnh ngày nóng chẳng hạn. Okie! Trang web của bạn để trực quan hơn thì nên có 1 cái biểu đồ để thể hiện nhiệt độ. Vậy nên ta có 1 đối tượng mới là "Chart". Nhiệm vụ của "Chart" là mỗi khi nhiệt độ thay đổi thì "Chart" sẽ vẽ lên 1 điểm mới trên biểu đồ.
Vậy để giải quyết bài toán này thì ta làm thế nào? Rất đơn giản, từ đối tượng "Chart", ta sẽ liên tục cập nhật "Temperature" của "Weather". Nếu như Temperature thay đổi, thì Chart sẽ thực hiện việc vẽ lên 1 điểm ở biểu đồ.
Okie! Nhìn có vẻ ổn đúng không? Nhưng sau một hồi ngẫm nghĩ, bạn lại muốn có thêm 1 cái bảng thống kê ghi lại những giá trị và thời gian thay đổi của Temperature. Vậy là bạn lại tạo ra 1 đối tượng "Table", và cũng làm y như "Chart" là cập nhật giá trị "Temperature".
Rồi bạn lại nhận ra, nên có thêm 1 hình animation thể hiện mức độ nóng hay lạnh ở thời điểm hiện tại, và bạn lại tạo 1 đối tượng "Animation". Đối tượng này bạn làm y hệt 2 đối tượng "Table" và "Chart". Cứ sau mỗi 1 phút, 3 đối tượng "Table", "Chart" và "Animation" lại gửi request cho "Weather" để cập nhật. Lúc này, bạn đã thấy có gì đó sai sai chạm môi bạn rồi đấy.
 Nhưng nhu cầu thông tin về thời tiết không chỉ có nhiệt độ, nó còn có về độ ẩm, về gió, về lượng mưa.... Thế nên, bạn lại thêm vào những biến "Wind", "Rain".... cho "Weather". Và tất nhiên, những đối tượng "Chart", "Table", "Animation" cũng nên cập nhật sư thay đổi của những thông số mới này. Và chính ngay lúc này đây, chi phí cho việc cập nhật giá trị quá lớn đã làm cho chương trình của bạn bị chậm đi. Nhưng chưa dừng lại ở đó, nhiều khách hàng còn muốn có thêm những tính năng khác như bản đồ thời tiết, lời khuyên thời tiết... Và vấn đề bạn chắc chắn sẽ gặp phải nếu cứ tiếp tục design theo cách này là tài nguyên của web bạn rất lớn và có thể bị crash. Và chính lúc này đây, bạn phải cần dùng đến observer pattern.
 Đơn giản là từ cách suy nghĩ "Pull", bạn phải suy nghĩ theo hướng "Push". Tức là, mỗi lần Temperature thay đổi giá trị, Weather sẽ tự động Push thông báo cho Chart , Table, Animation. Và theo kiểu suy nghĩ thiết kế này, chúng ta sẽ ko phải tốn tài nguyên cho việc liên tục gửi request để cập nhật nữa. Vậy để Weather biết phải thông báo cho ai khi Temperature thay đổi, thì những đối tượng như Chart, Table và Animation cần phải đăng kí vào Weather để nó còn biết mà la làng lên chứ. Đúng ko?
 Vậy, định nghĩa của observer Pattern là gì?
Observer Pattern: định nghĩa mối quạn hệ one-to-many giữa nhứng đối tượng với nhau. Sao cho, mỗi khi 1 đối tượng thay đổi, thì những đối tượng quan hệ với nó sẽ được thông báo và cập nhật tự động
Okie! Vậy thì chúng ta thực hiện Observer pattern như lào? Khái niệm thì có rồi đấy, nhưng mần như lào mới quan trọng. Trên mạng h có rất nhiều mô hình để hiện thực hóa cái loại pattern này, và mình chỉ lấy 1 cái ra để xài thôi. Nó sẽ được mần như lày:
Đầu tiên, bạn tạo 1 interface Observable , đây là cái interface cho những đối tượng như Weather trong ví dụ ấy. Interface này sẽ gồm các hàm chính như : add, remove và notify. Tiếp theo, bạn tạo 1 interface Observer cho những đối tượng cần được cập nhập ( như là Chart, Table, Animation). Interface này sẽ có hàm update.
Bây giờ, ta tạo 2 hiện thực cho 2 interface này. Mình sẽ tạo hiện thực Weather và Chart. Trong đó, Weather mình sẽ cho thêm 1 hàm là getTemperature(). Hàm này sẽ giúp cho Chart có thể lấy được thông tin mỗi khi cần dùng để update.
Vậy thôi, đó chính là mô hình dùng để hiện thực hóa Observer Pattern. Bây giờ, ta chỉ cần đăng kí Chart vào trong Weather bằng cách chạy hàm add(). Sau đó, mỗi lần temperature mà đổi, thì Weather sẽ chạy hàm notify(), hàm này sẽ chạy hàm update() của Chart. Vậy là xong, rất i zì.
Và giờ mình sẽ thêm tí code cho sinh động, cho mấy bro nghiền code có cái mà đọc nhá. Đầu tiên là tạo 2 cái interface cho observable và observer.
public interface IObservable{
    public void add(IObserver o);
    public void remove(IObserver o);
    public void notify();

}

public interface IObserver {
    public void update();
}
Sau đó mình tạo 2 hiện thực cho 2 cái interface này, đó là Weather và Chart.
public class Weather{
    private List listObservers = new ArrayList();
    private int temperature;
    public void add(IObserver o) {
         this.listObservers.add(o);
    }
    public void remove(IObserver o) {
         this.listObservers.remove(o);
    }
    public void notify() {
         for(IObserver o : listOBservers) {
              o.update();
         }
    }
    public int getTemperature() {
         return this.temperature;
    }
}

public class Chart{
    private Weather weather;
    public Chart(Weather weather){
        this.weather = weather;
    }
    public void update() {
         System.out.println("them 1 diem tren bieu do voi gia tri la: " + this.weather.getTemperature());
    }
}
Vậy là xong, h chỉ cần khởi tạo 2 đối tượng Weather và Chart là tắt máy đi ngủ được rồi.
Weather weather = new Weather();
Chart chart = new Chart(weather);
weather.add(chart);
Share:

Strategy Pattern

Nếu ai đó hỏi tui, pattern nào là dễ nhất! tui bợp ngay câu trả lời là : Strategy Pattern. Nếu người ta lại bắt tui miêu tả strategy pattern trong 1 câu ngắn gọn thì tui độp ngay cho 1 câu trả lời: đừng suy nghĩ theo kiểu kế thừa, mà hãy suy nghĩ theo kiểu hiện thực. Vậy, định nghĩa ( 1 cách máy móc và khó hiểu) của strategy pattern là gì?
Strategy pattern: định nghĩa một danh sách các thuật toán và đóng gói chúng để giúp chúng có thể thay đổi được. Pattern loại này giúp cho các thuật toán được định nghĩa sẳn có thể được thay đổi bởi người sử dụng chúng.
Oài, quăng ba cái định nghĩa đó đi. Đọc vào chỉ rối não chứ chả hiểu gì đâu. Nên theo chân mình, để mình dẫn bạn vào thế giới của những ví dụ.
 Giả sử, bạn có 2 con vịt: 1 con là vịt nhà, 1 con là vịt rừng. Cả 2 con này đều kêu "quạc quạc", đều có thể bay, và đều có thể nhìn thấy được. Và ta có bản thiết kế sau:

Đúng như những gì ta được học về kế thừa rồi đúng ko? Nhưng sẽ chẳng có gì to tát nếu bỗng nhưng trên trời rơi xuống thêm 1 con vịt cao su. Con vịt cao su này thì lại không biết bay, vậy là ta phải thiết kế lại.

Nhìn có vẻ ổn rồi nhể? Nhưng chưa đâu anh bạn, khách hàng kêu là: "ê chú, tui mới đặt thêm 2 loại vịt nữa. 1 con là vịt trời, 1 con là vịt núi, 2 con này nó cũng bay được bình thường, nhưng mà nó bay khác với con vịt nhà với vịt rừng chú ạ! chú coi coi sao sửa lại dùm tui gấp nhá!". Và lúc đó bạn kiểu: "ơ, goắt đờ hợi? Lại sửa à?". Vừa mới mở bản thiết kế lên để chuẩn bị chèn vào 2 con vịt chó đẻ thì ông khách hàng lại hớn hở chạy vào:
- Ê chú ơi, bên kia người ta lại sắp giao thêm vịt xi măng nữa chú ạ. Vịt xi măng thì không biết bay và cũng chả biết kêu nhá.
- Cút!!!!
Đấy, bạn thấy đấy. Lúc đầu kế thừa có vẻ là hay. Nhưng về lâu về dài thì nó lại có vấn đề. Khi gặp nhiều thay đổi thì cái cây kế thừa của bạn càng ngày càng mở rộng. Và khi bạn mở rộng quá dài thì nhận ra là 2 con vịt ở 2 nhánh cách xa nhau lại có 1 đặc điểm chung và lúc này bạn phải ngậm ngùi để cho code của bạn bị lặp lại. Và kết quả là code của bạn nhìn như cứt ấy. (Nói thẳng ra là vậy! =)) )
Vậy để giải quyết vấn đề này thì ta sẽ dùng Strategy pattern.
Ban đầu, ta có 1 con vịt. 1 con vịt thì biết bay, và nó biết kêu. Okie. Ta sẽ tạo 1 con vịt như thế, NHƯNG! ta sẽ thiết kế hành động bay và kêu này là những interface. Nghĩa là sao? nghĩa là con vịt của chúng ta có hành động bay và kêu đấy, nhưng nó chỉ là interface mà thôi, nó không có làm được gì nêu như ta chưa định nghĩa cho nó.

Tiếp đến, ta bắt đầu định nghĩa cho những interface này. Ta có con vịt nhà và vịt rừng bay kiểu nhẹ nhàng. Còn con vịt trời và vịt núi bay kiểu mạnh mẽ. Con vịt cao su và vịt xi măng thì không biết bay. Tức là ta có 3 hiện thực cho interface bay.

Tương tự với tiếng kêu của vịt cũng thế. Ta có 2 hiện thực là kêu và không kêu.

Và bây giờ để tạo ra con vịt nhà thì ta chỉ cần truyền vào cho đối tượng "DUCK" 2 hiện thực là "Smooth fly" và "Normal quack". Muốn tạo vịt xi măng thì truyền vào "DUCK" 2 hiện thực là "No fly" , "No quack". Và đây chính là strategy pattern.
 Bây giờ thử code vài dòng xem nó như thế nào xem nào?
 Đầu tiên ta tạo 2 cái interface bay và kêu.
public interface IFly {
    public void fly();
}

public interface IQuack {
    public void quack();
}
 Sau đó ta hiện thực những interface này. Đầu tiên là những lớp bay.
public class NoFly implements IFly{
    public void fly(){
         // khong lam gi ca
    }
}

public class SmoothFly implements IFly{
    public void fly(){
         Systems.out.println("bay nhe nhang!");
    }
}

public class StrongFly implements IFly{
    public void fly(){
         Systems.out.println("bay manh me!");
    }
}
 Sau đó là những lớp kêu.
public class NoQuack implements IQuack{
    public void quack(){
         // khong lam gi ca
    }
}

public class NormalQuack implements IQuack{
    public void quack(){
         Systems.out.println("Quac quac!");
    }
}
 Và giờ ta có thể tạo đối tượng vịt dựa trên 2 interface IFly và IQuack
public class Duck {
    private IFly flyBeha;
    private IQuack quackBeha;
    public Duck (IFly flyBeha, IQuack quackBeha){
        this.flyBeha = flyBeha;
        this.quackBeha = quackBeha;    
    }
    public void quack(){
         this.quackBeha.quack();
    }

    public void fly(){
         this.flyBeha.fly();
    }
}
 Đối tượng đã có hết rồi, h muốn tạo con vịt nào là có con vịt nấy.
Duck CityDuck = new Duck(new SmoothFly(), new NormalQuack());
Duck RubberDuck = new Duck(new NoFly(), new NormalQuack());
Duck CementDuck = new Duck(new NoFly(), new NoQuack());
 Strategy pattern giúp chúng ta thiết kế 1 cách tùy biến hơn, linh động hơn và giúp ta làm chủ hơn việc khởi tạo đối tượng. Với phương pháp này thì cái cây kế thừa của chung ta sẽ bị dẹp qua 1 bên và không còn phải lo lằng nhiều nếu như có thêm 1 con vịt chó chết nào được thêm vào nữa. Giả dụ chúng ta có thêm con vịt điên chẳng hạn, nó không biết kêu mà lại biết bơi. Thì cũng chỉ cần truyền vào đối tượng "DUCK" những hiện thực tương ứng thôi. Và từ đây thì cũng có thể thấy được là, tính kế thừa cũng chả có quá đỉnh cao như chúng ta vẫn được học, quan trọng là kế thừa lúc nào mới là hiệu quả.
Share:

Thứ Sáu, 23 tháng 11, 2018

Image Classification

Chào mừng bạn đến với các bài học Image classification, một trong những bài học thuộc mảng Computer Vision được mình viết trên blog cá nhân. Mong rằng qua những bài học trong series này, bạn sẽ nắm vững được một số kiến thức cơ bản trong việc phân loại hình ảnh sử dụng các phương pháp máy học, để từ đó có thể làm tiền đề cho những ứng dụng sau này của bản thân.
Bài 1 : Giới thiệu
Bài đầu tiên sẽ giới thiệu tổng quan về series image classification.
Bài 2 : Image classification
Bài này sẽ giới thiệu tổng quan về image classification
Bài 3 : Thuật toán Nearest Neighbor Classifier
Thuật toán phân lớp Người láng giềng gần nhất
Bài 4 : Cài đặt Nearest Neighbor Classifier với ngôn ngữ Python
Bài 5 : Thuật giải K-Nearest Neighbor
Bài 6 : Cài đặt K-Nearest Neighbor Classifier với ngôn ngữ Python
 Bắt đầu từ đây, các bài tiếp theo sẽ dính dáng khá nhiều tới toán học nhé mấy man. Nếu không hiểu công thức toán thì cứ kệ đi nhá :v Vì biết đâu đọc ví dụ lại hiểu. Đó cũng là cách mình đọc tài liệu ( vì mình không giỏi toán lắm).
Bài 7 : Score function
Bài 8 : Loss function
Bài 9: Optimization và Gradient Descent
Bài 10: Backpropagation
Bài 11: Giải thích quá trình máy học
Như vậy, chúng ta đã hiểu máy học là gì. Những phần tiếp theo, chúng ta sẽ tìm hiểu về model. Những model được đem vào mổ sẻ là ANN (mạng nơ ron nhân tạo) và CNN (mạng nơ ron tích chập).
Bài 12: Kiến trúc mạng neural nhân tạo
Bài 13: Cấu trúc của 1 neural
Bài 14: Tìm hiểu mạng CNN
 Okie, Vậy là chúng ta đã hoàn thành được bài CNN, cũng là bài cuối trong loạt series image classification của mình. Đây chỉ là kiến thức nền cơ bản để các bạn hiểu và nắm được các khái niệm của Classification và những mạng model dùng để giải quyết bài toán đó. Mong các bạn dựa vào đây có thể tự mình tìm hiểu và phát huy được bản thân trong lĩnh vực đầy thú vị này. Chào thân ái và quyết thắng.
Share:

Tìm hiểu về Convolutional Neural Network (CNN)

Chúng ta đã được tìm hiểu về mạng neural nhân tạo trong bài này. Nếu như đầu vào của chúng ta là 1 bức hình 30*30*3 pixel, thì lớp input của neural network sẽ là 30*30*3 neural. Số lượng neural này vẫn tạm chấp nhận được cho quá trình training vì chi phí tính toán cũng chưa phải là lớn lắm. Nhưng nếu đầu vào của chúng ta là bức hình 1000*1000*3, thì số lượng neural lúc này cực kì lớn và quá trình tính toán sẽ chậm đi rất nhiều, đồng thời mạng phức tạp sẽ dễ dẫn đến overfitting.
Mặt khác, với đối tượng đầu vào là bức hình, thì các pixel gần nhau hợp lại sẽ mang những thông tin hữu ích, nhưng neural network thì không khai thác được đặc điểm này. Do đó, có một mạng neural mới, được người ta nghiên cứu sáng tạo ra: đó là convolutional neural network (CNN). Vậy CNN khác với ANN như thế nào? Hiểu một cách nôm na thì CNN không đọc từng pixel riêng lẻ như ANN, mà nó sẽ đọc 1 loạt các pixel gần nhau để đưa vào tính toán. Nhưng để hiểu rõ hơn về cách hoạt động của CNN, chúng ta phải đi vào tìm hiểu từng thành phần cấu tạo nên mạng CNN mới rõ đc. Ở đây chúng ta sẽ tìm hiểu các khái niệm mới như Convolutional Layer, Pooling Layer.

Convolutional Layer:

Convolutional Layer là 1 lớp cực kì quan trọng trong CNN, nó đảm nhận hầu hết chức năng tính toán của mạng.
vi du mang cnn
Để giải thích về Convolutional Layer, mình sẽ đi thẳng trực tiếp vào ví dụ để giải thích cho trực quan.
Đầu tiên là khái niệm filter map: thay vì kết nối với từng pixel của hình ảnh đầu vào như ANN, CNN có những tấm filter dùng để áp vào những vùng của bức hình. Các filter map này thực chất là một ma trận 3 chiều gồm các con số. Và điều bạn cần phải cực kì lưu ý là các con số này chính là các parameter cần phải học. Filter map có kích thước dài và rộng là hyperparameter ( hyperparameter là gì thì mình có nói trong bài này) , riêng chiều cao ( mình sẽ gọi là depth - chiều sâu ) của filter map sẽ bằng với depth của lớp trước.
Ví dụ nhìn vào hình ví dụ ở trên, chúng ta có 2 filter map màu hồng là W0 và W1. Mỗi filter map có kích thước 3*3*3. Kích thước dài * rộng ( 3*3 ) là hyperparameter, còn chiều sâu (Depth) sẽ bằng với depth của input volumn, tức là bằng 3.
Stride: như đã nói ở trên, filter map được dùng để trượt lên tấm hình đầu vào, vậy trượt ở đây có nghĩa là như thế nào? Rất đơn giản, chỉ cần dịch filter map theo pixel từ trái sang phải theo từng dòng. Mỗi lần dịch như thế sẽ dựa vào 1 giá trị gọi là stride.
Ví dụ, stride = 1, thì mỗi lần dịch filter map sẽ sang phải 1 pixel, khi hết cạnh biên phải thì xuống 1 dòng và dịch tiếp. Còn nếu stride = 2 thì mỗi lần dịch sẽ sang phải 2 pixel, khi hết cạnh thì xuống 2 dòng.  
Padding: người ta sẽ thêm những giá trị 0 bao quanh lớp input. Ví dụ như ở trên hình, lớp input của chúng ta ban đầu có kích thước 5*5*3, nhưng vì giá trị padding = 1, nên được bao thêm 1 lớp 0 bên ngoài. Từ đó, kích thước của lớp input = 7*7*3.
Feature map: feature map thực chất là kết quả sau khi lớp input được filter map quét qua hết. Với mỗi lần filter map áp lên input, sẽ có quá trình tính toán xảy ra. Vậy quá trình tính toán này là gì? Thực chất đó là quá trình nhân 2 ma trận.
Ví dụ như trên hình, khi lớp trên cùng của filter map W0 áp vào lớp trên cùng của input volum, ma trận filtermap w0 3*3 sẽ được nhân với ma trận 3*3 của input sau đó cộng thêm bias=1, và đưa ra kết quả = 2 ở feature map. Kích thước feature map được tính như nào? Mỗi lần 1 filter map trượt hết lên input volum sẽ cho ra 1 lớp của feature map. Vậy nên, depth của feature map sẽ bằng số lượng filter map.
Ví dụ như hình trên, depth của feature map = số lượng filter map = 2. Còn chiều dài và chiều rộng của feature map sẽ được tính bằng công thức: (W + 2P - F)/S +1 . Trong đó, W là kích thước input, P là padding, F là kích thước filter map, S là stride.
Ví dụ với hình, W = 5, P =1, F = 3, S =1 , ta tính được kích thước của feature map là 3.
Ta có thể thấy được, nếu với ANN thông thường, 1 bức hình 7*7*3 như trên sẽ cần có 7*7*3 = 147 parameters (nếu lớp thứ 2 chỉ có 1 neural), nhưng với CNN lại cần ít hơn số parameter từ filter map và số lượng parameter này có thể được chúng ta định lượng.

Pooling layer

Thường giữa các lớp Convolutional Layer với nhau người ta sẽ chèn vào 1 lớp Pooling layer để giảm bớt số lượng parameter lại nếu như đầu vào quá lớn. Có nhiều loại pooling layer, nhưng mình chỉ giới thiệu max pooling.
  vi du mang pooling
 Ví dụ ở trên, lớp max pooling có kích thước là 2*2, stride = 2. Với mỗi lần áp lên input, nó sẽ lấy giá trị lớn nhất trong khoảng filter đó và để nó vào output.

ReLU layer

Khái niệm về ReLU mình đã nói trong bài này rồi, các bạn quay lại để xem nhá. Thường thì sau khi feature map được tính ra, người ta sẽ xếp theo sau đó 1 lớp ReLU, lớp này sẽ áp dụng hàm ReLU lên tất cả các giá trị của feature map.

Fully Connected Layer

Để đưa ra được kết quả dự đoán thì lớp Neural network sẽ được thêm vào sau một mang CNN. Lớp neural này cũng chỉ là ANN bình thường thôi chứ ko khác gì cả. Vậy một mạng CNN thông thường sẽ có cấu trúc các lớp như thế nào? Sau đây là ví dụ:
INPUT -> [[CONV -> RELU]*N -> POOL?]*M -> [FC -> RELU]*K -> FC
Share:

Cấu trúc của 1 neural

neurals
Mỗi neural sẽ nhận vào các input. Các input này có thể là giá trị của tập dataset đưa vào (ví dụ như các điểm ảnh), hoặc input này cũng có thể là các giá trị output của layer trước đó. Mỗi input đi vào sẽ kèm với 1 giá trị Weight. Weight này có thể hiểu là 1 cái cầu nối giữa input và neural.
Lấy ví dụ như cái mạng nhện trong nhà bạn ấy, nhà bạn nào sạch quá ko có mạng nhện thì qua nhà hàng xóm xin coi cái mạng nhện như nào. Những cái weight này giống như mấy đường nối của mạng nhện, còn mấy cái điểm giao của mạng nhện cũng như là neural. Ok chưa?. (thực ra chả liên quan gì đâu :/ ).
Nói vòng vo nãy h, vậy cuối cùng neural là gì, có nhiệm vụ làm gì? Các bạn cứ hiểu neural là cái phòng để tính toán xử lí các dữ liệu dựa vào input và weight. Ở đây, nó sẽ làm phép tính tổng của các tích WiXi (Trong đó Wi là weight, và Xi là input). Nghe quen không? Nó y chang như cái hàm linear classifier trong bài function score đấy.
$F = Wx .$
Sau khi qua căn phòng tính toán. Giá trị sẽ được đi qua 1 hàm transfer function ( hay Activation function). Hàm này sẽ tiếp tục xử lý tùm lum tá lả để đưa ra 1 giá trị output. Vậy hàm activation function này là gì?

Activation function:

Thực ra activation function là một hàm toán học thôi, định nghĩa vậy là dễ hiểu nhất. Có rất nhiều loại Activation function nhé, sau đây mình sẽ giới thiệu qua một số hàm activation function chính, và mình cũng giới thiệu công thức thôi. Còn mấy vụ giải thích mặt toán học để xem xét nó xịn chỗ nào, nó cùi chỗ nào thì mình sẽ ko bàn tới nhé.  
Sigmoid: Hàm này có công thức:
$\sigma (x)= \frac{1}{1+e^{-x}}$
Giá trị x sau khi đi vào hàm này sẽ cho kết quả là 1 giá trị trong khoảng 0 – 1.
sigmoid
Tanh:
$tanh(x)=2\sigma (2x)-1$

Hàm này sẽ cho ra giá trị trong khoảng -1 đến 1
  tanh
ReLU:
$f(x)=max(0,x)$
Hàm này lấy giá trị của x nếu x>0. Nếu x<0 0.
ReLU
Ngoài ra còn có rất nhiều hàm activation function khác mà mình sẽ không kể đến. Mỗi hàm sẽ có ưu nhược điểm riêng, tùy theo bài toán giải quyết mà người ta sẽ lựa chọn cho phù hợp.
Share:

Kiến trúc mạng neural nhân tạo

Kiến trúc của một mạng neural

Mạng neural nhân tạo, Aritificial Neural network (viết tắt là ANN). Đây là mạng đang được nhắc đến siêu nhiều trong các bài quảng cáo về công nghệ AI, bạn chắc hẳn đã gặp đâu đó những câu từ như: “chúng được xây dựng dựa trên công nghệ AI, công nghệ này áp dụng một mạng nơ ron nhân tạo để học dữ liệu.. bla bla”. Vậy rút cuộc cái ANN là gì? Dưới đây là một mô hình tổng quan ANN.
neural 1
Một ANN có 3 thành phần :
Input layer: là lớp đầu vào, như mô hình trên thì lớp đầu vào nhận 3 giá trị.
Hidden layer: lớp ẩn. Lớp này nhận giá trị từ lớp liền kề trước. Sau đó xử lí các thứ các thứ rồi truyền tiếp đi lớp sau (Xử lí gì thì chúng ta sẽ tìm hiểu sau, giờ cứ hiểu vậy đã). Một mạng ANN có thể có nhiều hidden layer.
Output layer: lớp đầu ra, là cái lớp xuất ra giá trị sau khi tính toán .

Các khái niệm cơ bản:


neural 2
Neural: Mỗi cái cục tròn tròn trong hình là 1 neural. Cục này có nhiệm vụ xử lý nhân cộng các giá trị rồi đưa ra kết quả. Cục neural này sẽ được giới thiệu trong bài này.  
Cách đếm số lớp của ANN: Khi đếm số lớp của ANN, chúng ta không đếm lớp input. Ví dụ mạng bên trái là 2 lớp, mạng bên phải là 3 lớp.  
Số lượng neural của mạng: số lượng neural được tính bằng tổng số neural của hidden layer và output layer. Ví dụ mạng bên trái là 6 neurals, còn mạng bên phải là 9 neurals.  
Số lượng parameters của mạng: cái này thì tùy vào bạn có muốn gộp chung Weight và bias hay không. Số lượng bias bằng số lượng neural. Số lượng weights bằng số lượng gạch nối. Mình thì sẽ tính số lượng parameter bằng tổng số weight và bias. Ví dụ mạng bên trái là 20 parameters ( không tính bias) và bên phải là 32 parameters (không tính bias).  
Fully connected Layer: là loại layer mà tất cả các neural của layer trước liên kết với tất cả neural của layer kế nó. Ví dụ cả 2 mạng trên đều là fully connected Layer.
Share:

Giải thích quá trình máy học

Sau quá trình học được score function, loss function, optimization (gradient descent), backpropagation. Mình sẽ giải thích cho mọi người quá trình máy học dựa trên những khái niệm này.
qua trinh hoc
 Đầu tiên, một bức ảnh sẽ được đưa vào model dự đoán của chúng ta. Model ở đây thực chất là tập hợp các Weight và bias. Khi model này nhận input là bức ảnh, nó sẽ tính toán dựa theo Score function và đưa ra được kết quả dự đoán. Kết quả dự đoán này sẽ được so sánh với kết quả thực để tính ra được độ lỗi theo loss function. Toàn bộ quá trình này là quá trình forward.
 Tiếp đến, quá trình backpropagation bắt đầu. Khởi động quá trình tính đạo hàm riêng điên loạn cho toàn bộ W và b trong model. Sau khi tính được đạo hàm riêng, gradient descent sẽ được áp vào để cập nhật các W và b. Và vòng lặp thứ nhất kết thúc.
Vòng lặp thứ 2 bắt đầu! Bức ảnh lại được đưa vào, model lại tiếp tục tính toán theo score function. Nhưng vì ở vòng lặp thứ nhất, các W và b đã được cập nhật theo gradient descent, do đó ở lần lặp này, loss function được tính ra sẽ thấp hơn loss function ở lần lặp trước. Điều này đánh dấu cho một sự kiện quan trọng của học máy: qua quá trình lặp, kết quả dự đoán đã chính xác hơn. Tiếp đến sẽ lại là quá trình backpropagation và cập nhật model. Quá trình này lặp cho đến khi nào loss đủ nhỏ mà người dùng ưng ý thì sẽ dừng hoặc sẽ dừng sau 1 số hữu hạn vòng lặp đã được lập trình.
Và đó, tất cả quá trình lặp forward và backpropagation đó, là quá trình máy học. Quá đơn giản đúng không nào!
Share:

Backpropagation

Phần backpropagation này rất nặng về toán đạo hàm nhé. Nên ai chưa có kiến thức đạo hàm riêng thì bắt buộc phải học trước đi nhé.

Ý nghĩa của đạo hàm riêng

Giả sử ta có hàm f = xy. Theo đạo hàm riêng thì ta có:
$\frac{\delta f}{\delta x}=y$ , $\frac{\delta f}{\delta y}=x$
Ví dụ với x= 4 và y =-3. Quá trình forward là quá trình tính toán theo biểu thức để ra kết quả. Tức forward ở đây là 4.-3 = -12. Lúc này, ta có đạo hàm riêng theo x = -3. Điều này có nghĩa là, nếu ta tăng giá trị của x 1 khoảng h nào đó, thì toàn bộ hàm f sẽ giảm một khoảng là -3h. Tương tự với đạo hàm riêng theo y = 4. Điều này có nghĩa, nếu ta tăng giá trị y 1 khoảng h nào đó, thì toàn bộ giá trị của f sẽ tăng 1 khoảng 4h. Như vậy, có thể hiểu:
Đạo hàm riêng của 1 biến chỉ ra được sự ảnh hướng của biến đó tới sự biến động của toàn bộ hàm số.

Các ví dụ

Giả sử ta có biểu thức: f(x,y,z)=(x+y)z. Giả sử x = -2, y = 5, z = -4. Ta có thể vẽ biểu thức như sau:
back 1
Còn quá trình backpropagation là quá trình tính đạo hàm của từng thành phần bằng cách tính ngược từ kết quả lên. Có thể thấy: q = x+ y có đạo hàm riêng là -4. Do đó, ta có thể nói rằng: nếu x hay y tăng thì q sẽ tăng, và f sẽ giảm 1 khoảng 4 lần giá trị tăng.  
Ví dụ tiếp theo cho hàm sigmoid Giả sử ta có hàm sau (gọi là hàm sigmoid):
$f(x,y)=\frac{1}{1+e^{-(w_{0}x_{0}+w_{1}x_{1}+w_{2})}}$
Ta có thể biểu diễn nó ra được như hình sau:
back 2
Quá trình tính toán forward cực kì đơn giản, bạn có thể tự tính ra được như trong hình. Mình sẽ giải thích quá trình backpropagation cho trường hợp này:
Giả sử cho kết quả output có đạo hàm = 1 cho dễ tính. Ta backward 1 bước về phép tính 1/x. Đạo hàm riêng của 1/x = -1/x^2 . Do đó ta có kết quả đạo hàm riêng tại đây là : (-1/1.37^2)*1 = -0.53. Tiếp đến ta backward 1 bước về phép tính x +1 . Đạo hàm theo x = 1. Do đó kết quả đạo hàm riêng tại đây = 1*-0.53 = -0.53. Tiếp tục backward lại 1 bước về phép tính :e^x . Ta có đạo hàm theo x của e^x = e^x. Do đó, đạo hàm riêng của x tại đây = e^-1*-0.53 = -0.2. Cứ thế, ta tính ngược cho đến input. Quá trình này chính là quá trình backpropagation.

Backpropagation và gradient descent

Quay lại với hàm score function, ta có $f(x_{i},W)=Wx_{i}$, loss function(SVM loss) $L_{i}=\sum_{j\neq y_{i}}max(0,s_{j}-s_{y_{i}}+\Delta )$. Giả sử W là tập hợp của 1 trăm triệu weight từ W1 đến W100 triệu. Và qua quá trình backpropagation của hàm L, ta có đạo hàm riêng của $\frac{\delta L}{\delta W_{69}}=-4$. Điều này có ý nghĩa là: Nếu W69 tăng 1 khoảng h, thì L sẽ giảm 1 khoảng -4h. Đúng chưa nào? Mục tiêu chúng ta đặt ra từ đầu là làm cho L giảm (loss càng thấp thì dự đoán càng chính xác).
Tiếp tục ôn bài gradient descent nè: để L giảm thì ta xài gradient descent với công thức $W_{t+1} = W_{t}-\alpha f'(W_{t})$. Với $\alpha > 0$. Áp vào cho W69, ta có : $W'_{69} = W_{69} - \alpha(-4)$. Tức là W69 sẽ tăng sau vòng lặp này, đồng nghĩa là L sẽ giảm sau vòng lặp này.
 Và bây giờ, hiệu ứng quá rõ ràng: nhờ backpropagation tính ra đạo hàm riêng cho W, mà ở bước lặp tiếp theo, W sẽ thay đổi dựa vào gradient descent. Và sự thay đổi này làm L ở vòng lặp tiếp theo giảm. Tức là sự dự đoán ở vòng lặp tiếp theo sẽ chính xác hơn!
Share:

L2 Regularization

Trong máy học, vấn đề nhức nhối bao nhiêu con người ưu tú của nhân loại là Overfitting, do đó, có rất nhiều người đêm quên ăn, ngày ăn bù để suy nghĩ làm sao để hạn chế được Overfitting. Và một ý tưởng chợt lóe ra, ý tưởng này có thể mường tượng được như sau: bạn có một model với tập hợp các parameters, parameters càng nhiều thì model càng phức tạp. Đúng chưa? mà model càng phức tạp thì overfitting càng dễ gặp. Vậy để giảm overfitting, ta cần giảm độ phức tạp model, để giảm độ phức tạp model, ta giảm số lượng parameters. Mà để giảm số lượng parameters thì ta làm sao? Ta phải cho các parameters có giá trị càng gần 0 càng tốt. Vì sao? Vì nếu càng gần 0, thì sức ảnh hưởng của parameters càng ít và gần như tiêu biến. Và đó, là cách giảm parameters.
Ý tưởng đã có, vậy làm thế nào để giảm được giá trị của W mà độ chính xác của model vẫn ngon lành. Đây là lúc L2 Regularization có tác dụng. Ví dụ với hàm Linear Classifier, ta có hàm tổng quan là :
$f(x_{i},W)=Wx_{i}$
Giả sử ta có input x = [1,1,1,1], ta có 2 W lần lượt là W1 = [1,0,0,0] và W2 = [0.25,0.25,0.25,0.25]. Cả 2 đều cho ra cùng kết quả là 1. Và vì chung kết quả từ score function nên loss function SVM cũng tính ra chung 1 kết quả theo công thức:
$L_{i}=\sum_{j\neq y_{i}}max(0,s_{j}-s_{y_{i}}+\Delta )$
Nhưng các bạn thấy, rõ ràng ở W2, các giá trị gần 0 hơn, do đó sẽ giảm được overfitting. Vậy nên người ta đã nghĩ ra cách kết hợp hàm L2 Norm có công thức như sau:
$R(W)=\sum_{k}\sum_{l}W_{k,l}^{2}$
Nhìn công thức hầm hố vậy thôi, thực ra chỉ là tổng bình phương các giá trị của W. Kết hợp với SVM loss ta có:
$L_{i}=\sum_{j\neq y_{i}}max(0,s_{j}-s_{y_{i}}+\Delta )+\lambda \sum_{k}\sum_{l}W_{k,l}^{2}$
Ta có R(W1)=1, R(W2)=0.25. Vậy nên, L2 < L1. Do đó, với hàm loss này, ta sẽ chọn L2, chính là chọn W2. Và đó, đó chính là cách L2 Regularization được áp dụng để hạn chế overfitting. Với việc lập trình, mình thấy người ta hay dùng thuật ngữ Weight decay, và weight decay cũng chính là L2 Regularization.
Share:

Optimization và Gradient Descent

Chúng ta đã tìm hiểu về score functionloss function. Giả sử score function có công thức $f(x_{i},W)=Wx_{i}$, và loss function (SVM loss) có công thức: $L_{i}=\sum_{j\neq y_{i}}max(0,s_{j}-s_{y_{i}}+\Delta )$
Mục tiêu của chúng ta là phải làm sao cho giá trị loss càng thấp càng tốt ( tức là dự đoán càng chính xác càng tốt). Trong công thức trên, $x_{i}$ và $y_{i}$ là cố định (đầu vào - đầu ra), và chỉ có W là có thể thay đổi trong mô hình. Vậy để giảm loss thì chỉ có cách đánh vào W mà thôi. Và Optimization chính là thứ giúp ta điều chỉnh được các giá trị của W để làm giảm được giá trị của loss.
 Nói suông là thế, vậy Optimization làm cách nào để điều chỉnh W giúp làm giảm loss? Sau đây là hướng tiếp cận để làm điều đó.  
Đầu tiên: Hướng tiếp cận đơn giản nhất cho Optimization là random.
Ý tưởng cực kì đơn giản. Để biết được tập W nào có loss thấp, thì ta sẽ random các tập W và tính loss dựa trên tập W đó. Sau đó ta sẽ lấy W có kết quả loss thấp nhất và dung nó để dự đoán.  
Hướng tiếp cận thứ 2 là random hướng. Ý tưởng cái này cũng đơn giản chả kém. Tưởng tượng quá trình đi tìm W là bạn đang lầm lũi bước đi trong một cái sân golf rộng lớn và uốn lượn để tìm nơi có vị trí thấp nhất.
san golf
Hướng tiếp cận thứ nhất có thể hiểu là bạn được random liên tục các vị trí trong sân golf đó. Nhưng với hướng tiếp cận thứ 2, sau khi bạn được random vị trí đầu tiên, bạn sẽ chọn đó làm vị trí gốc để tiếp tục bước đi chứ không random tiếp nữa.
Giả sử bạn lần đầu bạn được thả rơi ngay trúng sườn đồi cỏ trong sân golf, bước tiếp đến bạn sẽ random một hướng để bước. Nếu bước tiếp theo bạn có vị trí cao hơn bước hiện tại (loss cao hơn) thì bạn sẽ bỏ qua và lại random hướng. Còn nếu bước tiếp theo bạn đi có vị trí thấp hơn bước hiện tại (loss thấp hơn) thì bạn sẽ đi. Quá trình này lặp đi lặp lại đến khi nào bạn ưng ý thì thôi.  
Hướng tiếp cận thứ 3 là dựa vào độ dốc
Ý tưởng hướng tiếp cận này là chúng ta dựa vào độ dốc nơi chúng ta đang đứng, sau đó sẽ đi theo hướng có độ dốc đi xuống. Ý tưởng trực quan thì là thế, nhưng về mặt toán học để hiểu được nó, bạn phải quay về các bài học về đạo hàm. Bài này của trang machinelearningcoban giải thích rất hay và dễ hiểu về đạo hàm trên phương diện toán học. Về cơ bản, chúng ta có thể hiểu trên một cái sân gofl lớn có rất nhiều địa hình nhấp nhô uốn lượn, và do đó có rất nhiều điểm thấp trũng . Những điểm này gọi là local minimum. Trong tất cả những local minimum này, điểm có giá trị thấp nhất gọi là global minimum. Việc tìm W để Loss function có giá trị nhỏ nhất cũng như là việc tìm điểm global minimum này vậy.
Nhưng trong thực tế, rất tiếc phải nói rằng việc này rất khó có thể thực hiện được, do vậy người ta chủ yếu chỉ tìm được local minimum mà người ta ưng ý thôi.
Quay lại với toán học, với hàm f(t), tại một điểm ti bất kì, nếu ta đi theo hướng ngược với hướng của đạo hàm f'(ti) thì sẽ đạt được vị trí có giá trị thấp hơn f(ti). Đây cũng chính là cách mà người ta áp dụng vào việc tìm W cho loss nhỏ.

Gradient Descent

Gradient là dốc, descent là đi xuống. Gradient descent là đi xuống dốc, tức là đi ngược với dấu đạo hàm. Giả sử ta đang ở vị trí W, và ở bước tiếp theo ta bước đi với khoảng cách $\alpha$, ta bước theo hướng ngược đạo hàm của f(w) thì ta có vị trí đứng tiếp theo là:
$W_{t+1} = W_{t}-\alpha f'(W_{t})$
Đây là công thức gradient descent , và được lặp rất nhiều trong quá trình học của máy học.

Phân loại Gradient Descent

Dựa vào số lượng của đầu vào cho mỗi lần lặp, người ta chia gradient descent làm 3 loại:  
Batch Gradient Descent (BGD) Với BGD, thì ở mỗi lần lặp, người ta sẽ sử dụng toàn bộ tập dataset để làm input cho mô hình. Có thể các bạn sẽ thắc mắc rằng, tại sao không thấy bất kì biến nào biểu thị cho dataset input trong công thức gradient descent. Nhưng thực ra, f’(Wt) thực chất là đạo hàm của score function: $f(x_{i},W)=Wx_{i}$. Và x ở đây chính là đầu vào của dataset. Như vậy, bạn có thể hiểu BGD tức là khi x = toàn bộ training set. Nhược điểm của phương pháp này là nó sẽ làm cho quá trình tính toán rất lâu và tốn tài nguyên.
 Stochastic Gradient Descent (SGD) Ở loại này, thì mỗi lần lặp, người ta chỉ sử dụng 1 đầu vào X duy nhất. Để dễ hiểu, giả sử bạn có 1 tập dữ liệu training set với 100 tấm hình, BGD là bạn sử dụng 100 tấm đó cho mỗi lần lặp. Còn SGD là bạn chỉ sử dụng 1 tấm cho mỗi 1 lần lặp.
Mini-batch gradient descent Phương pháp này thì mỗi lần lặp, bạn sẽ chọn ra ngẫu nhiên 1 tập gồm n phần tử từ tập training set. N sẽ lớn hơn 1 và nhỏ hơn tổng số phần tử của training set. Ví dụ bạn có 100 tấm hình trong training set, n có thể là 5, 10 hay 20. Nếu n = 1 thì sẽ là SGD, còn nếu n = 100 thì sẽ là BDG.
Share:

Loss Function

Trong bài Score Function, khi nhìn vào công thức của linear classifier: $f(x_{i},W,b)=Wx_{i}+b$ ta có thể thấy được rằng: $x_{i}$ là đại lượng không thể thay đổi được (vì nó là bức hình chúng ta đưa vào). Nhưng ta có thể thay đổi được các parameters (W và b). Từ đó có ý tưởng, chúng ta sẽ thay đổi các parameters, sao cho với bức ảnh vào, hàm f sẽ cho ra được kết quả dự đoán đúng với các nhãn dán. Ví dụ với kết quả dự đoán trong bài Score Function:
vi du
Có thể thấy rằng, kết quả đúng (cat) có giá trị thấp hơn so với kết quả sai (Dog, ship). Do đó, nảy sinh một ý tưởng rằng: ta sẽ xây dựng một hàm L nào đó có khả năng đánh giá được độ chính xác của hàm f. Giả sử nếu kết quả của hàm f sai càng nhiều, thì giá trị của hàm L càng cao. Nếu hàm f dự đoán càng đúng, thì hàm L càng thấp. Và hàm L này, người ta gọi nó là Loss function (hàm độ lỗi). 
Loss function (hàm độ lỗi) là hàm có giá trị cao khi kết quả dự đoán của f tệ, và có giá trị thấp khi kết quả dự đoán của f chính xác.
Loss function thì có rất nhiều hàm định nghĩa. Nhưng dưới đây là 2 loại mình sẽ giới thiệu trong bài này:

Multiclass Support Vector Machine loss (SVM)

SVM là hàm được xây dựng sao cho các giá trị của các nhãn đúng phải lớn hơn giá trị của các nhãn sai 1 khoảng Δ nào đó. Giả sử với bức hình thứ i, chúng ta có tập pixels $x_{i}$ và nhãn dán $y_{i}$. Score function sẽ nhận giá trị $x_{i}$ và tính toán thông qua hàm $f(x_{i}, W)$ . Giả sử ta quy ước rằng giá trị của nhãn thứ j sau khi được tính toán ra là $s_{j}=f(x_{i},W)_{j}$, thì lúc này hàm SVM cho bức hình thứ I được tính bằng:
$L_{i}=\sum_{j\neq y_{i}}max(0,s_{j}-s_{y_{i}}+\Delta )$
Ví dụ dễ hiểu nhất là: chúng ta có 3 giá trị tương ứng với 3 nhãn dán , sau khi được tính toán bởi hàm f là: s=[13,−7,11]. Và nhãn đúng là nhãn đầu tiên (yi=0) . Giả sử Δ = 10. Svm loss sẽ được tính bằng: Li=max(0,−7−13+10)+max(0,11−13+10)
Ở vế đầu, giá trị là 0, vì max (0,-10) = 0. Ở vế sau, max(0,8) = 8. Vậy nên, tổng loss Li = 0+8 =8. Giá trị này có ý nghĩa độ lỗi của hàm f là 8. Và cũng đúng với tiêu chí của định nghĩa loss function: nếu kết quả f càng tệ thì L có giá trị càng cao. Nói tóm lại là, svm loss sẽ bỏ qua cho giá trị của các nhãn sai bé hơn giá trị nhãn đúng 1 khoảng Δ. Còn nếu không, nó sẽ cộng giá trị sai này vào hàm loss.

Softmax classifier

Bên cạnh SVM loss, một hàm khác được sử dụng phổ biến ko kém là Softmax. Hàm softmax được tính theo công thức:
$L_{i} = -log(\frac{e^{f_{yi}}}{\sum _{j}e^{f_{j}}})$
Nhìn thì có vẻ kinh khủng thật. Nhưng mà đi vào ví dụ thì rất dễ hiểu.


Kết quả sau khi được tính từ score function, sẽ được đưa vào hàm $e^{f_{j}}$. Sau đó mỗi giá trị sẽ được tính thành giá trị xác suất. Và cuối cùng, giá trị loss = -log(giá trị của nhãn đúng). Vậy là xong, tổng kết bài này là bạn hiểu được loss function là gì. Hai hàm svm và softmax là gì. Vậy thôi.
Share:

Score Function

Score function là gì? Đơn giản mà hiểu:
Score function là một hàm có khả năng chuyển từng pixel trên 1 bức ảnh thành các điểm tin cậy (confidence score) tương ứng với mỗi nhãn dán.
Đọc khái niệm thì có vẻ khó hiểu. Nhưng cứ đi vào ví dụ thì mọi chuyện sẽ dễ dàng hơn. Giả sử ta có 1 tập training set gồm i bức hình $x_{i}\epsilon R^{D}$, mỗi bức hình có nhãn dán tương ứng là $y_{i}$. Trong đó i ∈ 1..N và $y_{i}$ ∈ 1..K . Tức là, chúng ta có 1 tập N bức hình (D chiều) và K nhãn dán phân loại. Ví dụ dễ hiểu: với tập CIFAR-10 ta có N = 50000 , D = 32*32*3 = 3072 điểm ảnh, K =10 nhãn dán. Vậy score function lúc này là 1 hàm sexcos khả năng chuyển các điểm ảnh $R^{D}$ về thành các điểm tương ứng với nhãn dán $R^{K}$ $f:R^{D}$↦$R^{K}$

Linear Classifier (Hàm tuyến tính)

Linear classifier là hàm cơ bản nhất (theo mình nghĩ), nó được viết như sau:
$f(x_{i},W,b)=Wx_{i}+b$
Với công thức trên, giả sử ta duỗi thẳng các pixels của 1 bức hình thành 1 vector duy nhất [D*1]. Ma trận W sẽ có kích thước là [K*D], vector b có kích thước là [K*1]. W và b này là các parameters của hàm. Ví dụ trong cifar-10, $x_{i}$ chứa tất cả pixels của bức hình thứ I, được duỗi thẳng ra thành 1 vector [3072*1] , W = [10*3072], b = [10*1]. Vậy với hàm trên, đầu vào sẽ là 3072 con số, và đầu ra sẽ là 10 con số. Nhìn lại định nghĩa của Score function dùm mình: Score function nhận đầu vào là 1 bức hình (thực chất là $x_{i}$ ), đầu ra là confidence score tương ứng với nhãn dán ( hàm $f(x_{i},W,b)=Wx_{i}+b$ cho đầu ra là một vector 10 con số với trường hợp là tập Cifar-10). Cũng nhắc luôn, W là weights, b là bias. Nhưng mình thường gọi chung hết W và b là parameters.

Ví dụ cho linear classifier


anh vi du
Ví dụ như ở bức hình trên, giả sử bức tranh đầu vào chỉ có kích thước 2*2 pixel, ta sẽ duỗi thẳng bức hình này ra thành vector $x_{i}$=4*1. Lúc này, D = 4. Giả sử trong trường hợp này số nhãn phân loại chỉ là 3 nhãn (cat, dog,ship) thì K = 3. Như vậy W lúc này sẽ là ma trận K*D = 3*4, b là vector K*1 tức là b = 3*1. Bây giờ, để tính ra được kết quả như hình trên, yêu cầu bạn phải biết được cách nhân và cộng ma trận. Cái này sẽ liên quan đến toán đại số tuyến tính. Và cũng có 1 lưu ý luôn cho mọi người đỡ phải bỡ cmn ngỡ về sau đó là: Deep learning nói chung và series Image classification nói riêng sẽ liên quan đến toán cực kì nhiều, nên hãy chuẩn bị tinh thần trước những công thức và tính toán hack não. Sau quá trình tính toán, kết quả đầu ra sẽ là 3 giá trị tương ứng với 3 nhãn. Vì dog có giá trị cao nhất nên được chọn. Và bạn cũng thấy được đây là kết quả sai, vì cat mới là nhãn đúng. Nhưng không sao, đây chỉ là ví dụ cho bạn hiểu được khái niệm linear classifier là gì thôi.

Ghép chung W và b trong toán học

Theo như công thức linear classifier ở trên, chúng ta đã tách biệt W và b. Nhưng thông thường khi lập trình và tính toán, người ta có thể gộp chung 2 phần tử trên thành 1 ma trận chung.
$f(x_{i},W)=Wx_{i}$
Việc gộp này thực hiện như thế nào, bạn nhìn vào hình sau sẽ rõ:
b sẽ được thêm vào sau ma trận W, lúc này ma trận W = K*D+1. Còn $x_{i}$ sẽ được thêm 1 giá trị 1 vào vector để có kích thước: $x_{i}$=D+1 Ok, vậy là xong. Qua bài này chúng ta đã hiểu được khái niệm cơ bản nhất của score Function. Đầu vào là ảnh, đầu ra là dãy số tương ứng với nhãn dán.
Share:

Cross Validation

Bài này sẽ giới thiệu một khái niệm trong lĩnh vực máy học đó là Cross Validation, một phương pháp siêu phổ biến để hạn chế Overfittings trong huấn luyện mạng. Vậy Cross Validation là gì?
Chúng ta phải quay lại bài toán training model trong máy học. Như bạn đã biết, thường với một bộ dataset chúng ta sẽ có 3 tập: training set, validation set và testing set, trong đó training set dùng để huấn luyện, validation set dùng để test trong quá trình huấn luyện và testing set dùng để test cho model cuối cùng. Nhưng nếu dataset của bạn không có validation set thì sao? Bạn sẽ lấy gì để test trong quá trình huấn luyện? Chúng ta chắc chắn không được dùng testing set để kiểm thử trong huấn luyện, bởi điều này sẽ dẫn tới overfitting trên tập test. Chúng ta cũng không được dùng tập train để kiểm thử nốt, vì nó sẽ dẫn tới overfitting trên tập train. Vậy chỉ còn 1 cách, bạn sẽ lấy 1 phần của tập train ra làm tập validation. Đây cũng là 1 ý kiến hay! Nhưng, nếu tập train của bạn quá ít, việc lấy ra 1 phần của tập sẽ làm nó ít hơn và dẫn đến thiếu dữ liệu train. Điều này cũng dẫn tới overfitting. Vậy thì phải làm sao???? Đây chính là lúc bạn dùng tới Cross Validation.
Cross Validation là phương pháp chia nhỏ tập training ra thành N phần. Với mỗi lần train, mô hình sẽ sử dụng N-1 phần để train, sau đó test dựa trên 1 phần còn lại. Điều này sẽ giúp cho mô hình hạn chế gặp phải overfitting và giúp bạn tìm ra được những Hyper parameter tốt hơn. Để dễ hiểu, ta đi tới ví dụ sau. Với bộ dữ liệu CIFAR-10, bạn không hề có tập Validation.
Với phương pháp Cross Validation, bạn có thể chia nhỏ tập train ra thành 5 phần. Tổng số ảnh của tập train là 50000 ảnh => mỗi phần nhỏ sẽ có 10000 ảnh.
Cross Validation
Với mỗi lần train đầu, bạn lấy 4 fold đầu tiên để train. Sau đó để test, bạn sử dụng fold 5 để test. Qua lần train thứ 2, bạn lấy từ fold 2 đến fold 5 để train, rồi lại lấy fold 1 để test. Và đó, chính là Cross Validation. Có thể những lý thuyết ở trên sẽ chẳng giúp bạn hiểu được vai trò của Cross Validation, thế nên chúng ta hãy thử áp dụng nó vào bài toán image classification với tập CIFAR-10 bằng thuật toán K-Nearest Neighbor.
Với thuật toán này, vấn đề đặt ra là bạn phải chọn tham số K sao cho tốt nhất bằng cách thử nghiệm. Giả sử ta chọn K = 1. Đầu tiên ta sẽ lấy fold 5 để test với tập train là từ fold 1 đến fold 4. Và kết quả cho ra được là a1% độ chính xác. Lần thứ 2 ta sẽ lấy fold 4 để test, và dùng các fold còn lại để train. Kết quả lần này cho ra a2% độ chính xác. Ta thực hiện lần lượt với 5 fold, sẽ cho ra 5 kết quả từ a1 đến a5.
Cross Validation
Như vậy, với K=1, ta có kết quả bằng : (a1+a2+a3+a4+a5)/5. Tiếp tục thử nghiệm với các K khác, và ta tìm ra được K có kết quả tốt nhất. Từ đó có thể lấy K tốt nhất để test trên tập test mà không sợ Overfitting.
Share:
Được tạo bởi Blogger.