Blog

  • Dependence Injection với Dagger trong Android

    Dependence Injection với Dagger trong Android

    Giới thiệu

    bài trước mình đã giới thiệu về việc tiêm phụ thuộc thủ công trong android. Việc chèn phụ thuộc thủ công hoặt service locators trong ứng dụng Android có thể có vấn đề tùy thuộc vào quy mô dự án của bạn. Bạn có thể giới hạn sự phức táp dự án của bạn khi nó tăng lên bằng cách sử dụng Dagger để quản lý các sự phụ thuộc.

    Dagger tự động tạo code mà bắt chước code bạn sẽ viết thủ công. Bời vì code được sinh tự động tại compile time. Dagger hilt được giới thiệu trong android jetpack nó giúp đơn giản hóa code DI hơn so với Dagger. Nhưng do nó dựa trên Dagger nên trong bài này mình sẽ giới thiệu sơ lược về Dagger.

    Lợi ích khi sử dụng Dagger

    Dagger giúp bạn từ viết mã tẻ nhạc và mã soạn sẵn dễ tạo ra lỗi bằng cách:

    • Sinh AppContainer code(application graph) mà bạn đã triển khai thủ công trong phân DI.
    • Tạo factories cho các class có sẵn trong application graph.
    • Quyết định xem có nên sử dụng lại một phụ thuộc hoặc tạo mới một instance mới thông qua việc sử dụng các scopes

    Dagger tư động làm tất cả điều này tại thời giơn build miễn là bạn khai báo các sự phụ thuộc của một class và chỉ định cách đáp ứng chúng bằng cách sử dụng các annotation. Dagger sinh ra code tương tự những gì bạn viết thủ công. Dagger tạo một graph của các đối tượng, cái mà nó có thể tham chiếu để tìm kiếm cách cung cấp một instance của một class. đối với mọi lớp trong graph Dagger tạo một factory-type class mà nó sử dụng trong nội bộ để lấy các instance của loại đó.

    Tại thời điểm xây dựng, Daggeer xem qua mã của bạn và:

    • Builds và Validates các biểu đồ phụ thuộc, đảm bảo rằng:

      • Mọi đối trượng phụ thuộc có thể được thỏa mãn, vì vậy không có runtime exception
      • Không có chu trình phụ thuộc nào tồn tại, vì vậy không có vòng lặp vô hạn.
    • Sinh các class mà được sử dụng tại runtime để tạo các đối tượng thực tế và các phụ thuộc của chúng.

    Một usecase đơn giản trong Dagger: Generating một factory

    Tạo một factory đơn giản cho class UserRepository hiển thị trong sơ đồ sau:

    Screenshot from 2021-09-30 23-41-28.png

    Định nghĩa UserRepository như sau:

    class UserRepository(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }
    

    Thêm một @Inject annotation vào contructor của UserRepository vì thế Dagger biết cách tạo một UserRepository:

    // @Inject lets Dagger know how to create instances of this object
    class UserRepository @Inject constructor(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }
    

    Trong đoạn mã ở trên, bạn đang nói với Dagger rằng:

    1. Cách để tạo một instance UserRepository bằng phương thức khởi tạo có annotate @Inject.
    2. Các phụ thuộc của nó là: UserLocalDataSourceUserRemoteDataSource.

    Bây giờ Dagger biết cách tạo một instance của UserRepository, nhưng nó không biết cách tạo các phụ thuộc của nó. Nếu bạn cũng chú thích các lớp khác, Dagger biết cách để tạo chúng:

    class UserLocalDataSource @Inject constructor() { ... }
    class UserRemoteDataSource @Inject constructor() { ... }
    

    Dagger component

    Dagger có thể tạo một graph các phụ thuộc trong dự án của bạn mà nó có thể sử dụng để tìm ra nơi mà nó nên lấy những phụ thuộc đó khi họ cần. Để Dagger làm được điều này, bạn cần tạo một interface và annotate nó với @Component. Dagger tạo một container khi bạn sẽ làm việc với việc tiêm phụ thuộc thủ công.

    Bên trong interface Component, bạn có thể định nghĩa các function mà trả về các instance của class mà bạn cần(ví dụ. UserRepository). @Component nói Dagger sinh ra một container với tất cả các phụ thuộc được yêu cầu để đáp ứng. Đây được gọi là Dagger component ; nó chứa một graph mà bao gồm các đối tượng mà Dagger biết cách để cung cấp và các phụ thuộc tương ứng của chúng.

    // @Component makes Dagger create a graph of dependencies
    @Component
    interface ApplicationGraph {
        // The return type  of functions inside the component interface is
        // what can be provided from the container
        fun repository(): UserRepository
    }
    

    Khi bạn build project, Dagger sinh ra một implementation của interface ApplicationGraphcho bạn là: DaggerApplicationGraph. với bộ xử lý annotation của nó, Dagger tạo một dependence graph mà bao gồm các quan hệ giữa ba class(UserRepository, UserLocalDataSource, UserRemoteDataSource) với chỉ một ertry point: getting một instance UserRepository. Bạn có thể sử dụng nó như sau:

    // Create an instance of the application graph
    val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
    // Grab an instance of UserRepository from the application graph
    val userRepository: UserRepository = applicationGraph.repository()
    

    Dagger tạo một new instance của UserRepository mỗi khi nó được yêu cầu.

    val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
    
    val userRepository: UserRepository = applicationGraph.repository()
    val userRepository2: UserRepository = applicationGraph.repository()
    
    assert(userRepository != userRepository2)
    

    Thi thoảng, bạn cần có một instance duy nhất của một dependency trong một container. Bạn có thể muốn điều này cho một vài lý do:

    1. Bạn muốn các loại khác mà có loại này làm một dependency để chia sẻ cùng một instance, như là nhiều đối tượng ViewModel trong luông đăng nhập sử dụng cùng LoginUserData.
    2. Một đối tượng là đắt giá để tạo và bạn không muốn tạo một new instance mỗi lần nó được khai báo như một phụ thuộc(cho ví dụ, một JSON parser).

    Trong ví dụ, bạn có thể muốn có một instance UserRepository duy nhất có sẵn trong graph để mỗi khi bạn yêu cầu một UserRepository, bạn luôn lấy cùng một instance. Điều này hữu ích trong ví dụ của bạn vì trong một ứng dụng thực tế có graph phức tạp hơn, bạn có thể có nhiều đối tượng ViewModel phụ thuộc vào UserRepository và bạn không muốn tạo một instance mới của UserLocalDataResourceUserRemoteDataResource mỗi lần UserRepository cần được cung cấp.

    Trong tiêm phụ thuộc thủ công, bạn đã làm điều này bằng cách pass sung một instance của UserRepository vào contructors của các classs ViewModel; nhưng trong Dagger, bởi vì bạn không viết thủ công, bạn nói cho Dagger biết răng bạn muốn sử dụng same instance. Điều này có thể hoàn thành với scope annotations.

    Scoping với Dagger

    Bạn có thể sử dụng scope annotations để giới hạn tồn tại của một object trong suốt thời gian tồn tại component của nó. Điều này có nghĩa là cùng instance của một phụ thuộc được sử dụng mỗi khi kiểu đó cần được cung cấp cho một class nhận nó làm phụ thuộc.

    Dể có một instance duy nhất của UserRepository khi bạn yêu cầu repository trong ApplicationGraph, sử dụng same scope annotation cho interface @ComponentUserRepository. Bạn có thể sử dụng annotation @Singleton cái mà đã đi kèm với gói javax.inject mà Dagger sử dụng.

    @Singleton
    @Component
    interface ApplicationGraph {
        fun repository(): UserRepository
    }
    
    // Scope this class to a component using @Singleton scope
    @Singleton
    class UserRepository @Inject constructor(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }
    

    Trong cả hai trường hợp, đối tượng được cung cấp cùng một phạm vi được sử dụng để chú thích giao diện @Component. Do đó, mỗi khi bạn gọi applicationGraph.repository(), bạn sẽ nhận được cùng một phiên bản của UserRepository.

    val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

    val userRepository: UserRepository = applicationGraph.repository()
    val userRepository2: UserRepository = applicationGraph.repository()
    
    assert(userRepository == userRepository2)
    

    Chú ý: Đối với các class như Activity hay fragment không thể inject qua contructor bời vì các class này là hệ thống tự gọi => sử dụng field inject:

    class LoginActivity: Activity() {
        @Inject lateinit var loginViewModel: LoginViewModel
    }
    

    Kết luận

    Dagger giúp việc tiêm phụ thuộc trở lên đơn giản hơn bằng cách sinh code tự động như chúng ta làm bằng tay, việc quản lý các phụ thuộc cũng dễ dàng hơn khi sử dụng Dagger. Bài viết này đã nếu một vài điều cơ bản về cách hoạt động của Dagger, bài viết sau mình sẽ giới thiệu về Dagger Hilt cái mà giúp cho việc tiêm phụ thuộc còn đơn giản hơn Dagger.

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

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

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

    Mở đầu

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Lấy dữ liệu

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

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

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

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

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

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

    Chuẩn hóa dữ liệu

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Lưu ý rằng:

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

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

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

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

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

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

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

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

    Kết bài

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

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

  • Dependency Injection trong Android

    Dependency Injection trong Android

    Giới thiệu

    Dependency Injection là một trong những pattern giúp code tuân thủ nguyên tắc Dependency Inversion. Làm cho một class độc lập với phụ thuộc của nó mang lại nhiều ưu điểm khi phát triển, test. Android có một số thư viện giúp việc DI trở nên dễ dàng như Dagger, Dagger Hilt. Để hiểu và sử dụng được các thư viện này trong bài này ta sẽ tìm hiểu một số khái niệm cơ bản về Dependency Injection.

    Các nguyên tắc cơ bản của DI

    Dependency injection là gì?

    Các class thường yêu cầu tham chiếu đến các class khác. Cho ví dụ, một class Car có thể cẩn một tham chiếu đến class Engine . Những class được Car tham chiếu đến như Engine được gọi là Dependencies (sự phụ thuộc), và trong ví dụ này Car là class phụ thuộc vào một instance của class Engine để chạy.

    Có ba cách để một class lấy một object nó cần:

    1. Class xây dựng phần phụ thuộc mà nó cần. Trong ví dụ ở trên, Car sẽ tạo và khởi tạo một instance Engine của riêng nó.
    2. Lấy nó từ một nơi khác. Một số Android API như là Context getters và getSystemService(), cách này hoạt động.
    3. Cung cấp nó như một parameter. Ứng dụng có thể cung cấp những sự phụ thuộc này khi class được khởi tạo hoặc pass chúng vào các function mà cần từng phụ thuộc. Trong ví dụ ở trên, contructor của Car sẽ nhận Engine như một tham số.

    Tùy chọn thứ ba là Dependence injection. Với cách tiếp cận này bạn nhận sự phụ thuộc của một class bằng cách cung cấp chúng thay vì việc để instance class tự lấy chúng.

    Đây là một ví dụ không dependency injection, Đại diện cho một Car mà tạo ra sự phự thuộc Engine của riêng nó trong code như này:

    class Car {
    
        private val engine = Engine()
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.start()
    }
    

    Screenshot from 2021-09-30 20-12-42.png

    Đây không phải là một ví dụ về DI bởi vì class Car đang xây dựng Engine của riêng nó, điều này vi phạm nguyên tắc DI (Dependence inversion). Có vấn đề bởi vì:

    • CarEngine được liên kết chặc chẽ – một instance Car sử dụng một loại Engine, và không có subclass hay triển khai thay thế nào có thể dễ sử dụng. Nếu Car tạo ra Engine của riêng nó, bạn sẽ tạo hai loại Car thay vif tái sử dụng Car cho các động cơ loại Gas and Electric.
    • Việc phụ thuộc nhiều vào Engine làm cho công việc test gặp nhiều khó khăn hơn. Car sử dụng một instance của Engine, vì vậy ngăn bạn sử dụng các bản test trong các trường hợp khác nhau.

    Code sử dụng Dependence injection như dưới đây:

    class Car(private val engine: Engine) {
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val engine = Engine()
        val car = Car(engine)
        car.start()
    }
    

    Screenshot from 2021-09-30 20-12-42.png

    Function main sử dụng Car. bởi vì Car phụ thuộc vào Engine, ứng dung tạo một instance của Engine và sau đó sử dụng nó để xây dựng một instacne Car. Những lợi ích của phương pháp tiếp cận sự trên DI này là:

    • Khả năng tái sử dụng Car. Bạn có thẻ pass các implementation khác nhau của Engine tới Car. Cho ví dụ, bạn có thể định nghĩa một subclass mới của Engine được gọi là ElectricEngine mà bạn muốn Car sử dụng. Nếu bạn sử dụng DI, tất cả những gì bạn cần làm là pass một instance được cập nhật ElectricEngine subclass của Engine, và Car vẫn hoạt động không có bất kỳ thay đổi thêm.
    • Dễ dàng testing Car. Bạn có thể pas một test double đến test các kịch bản khác nhau của bạn. Cho ví dụ, bạn có thể tạo một test double của Engine được gọi là FakeEngine và cấu hình nó cho các bài tests khác nhau.

    Có hai cách chính để thực hiện Dependence Injection trong android:

    • Contructor injection. cách này được mô tả ở trên. Bạn pass sự phụ thuộc của class vào contructor của chính nó.
    • Field Injection( họăc Setter Injection). Một số lớp Android framework như là activity and fragment kà được khỏi tạo bởi hệ thống, vì vậy constructor injection là không thể. với field injection, Các sự phụ thuộc được khởi tạo sau khi class được tạo. Code sẽ giống như này:
    class Car {
        lateinit var engine: Engine
    
        fun start() {
            engine.start()
        }
    }
    
    fun main(args: Array) {
        val car = Car()
        car.engine = Engine()
        car.start()
    }
    

    Dependency injection tự động nhờ thư viện

    Trong ví dụ trước, ta đã tạo, cung cấp và quản lý các sự phụ thuộc của những class khác. Không cần dựa vào thư viện. Điều này được gọi là DI bằng tay, hoặc manual dependency injection. Trong ví dụ Car, chỉ có một sự phụ thuộc, nhưng nhiều sự phụ thuộc và các class có thể làm cho việc tiêm phụ thuộc thủ công trở nên tẻ nhạc hơn. Việc tiêm phụ thuộc thủ công cũng xảy ra một số vấn đề:

    • Cho các ứng dụng lớn, nhận tất cả sự phụ thuộc và kết nối chúng chính xác có thể yêu cầu một số lượng lớn mã mẫu ghi sẵn. Trong một kiến trúc nhiều lớp, để tạo một đối tượng cho một top layer, bạn phải cung cấp tất cả sự phụ thuộc của các layer dưới nó. Một ví dụ cụ thể, để build một oto thật bạn có thể cần một engine, một bộ phận truyền tải, một khung xe và các bộ phận khác; và một engine lần lượt cần xilanh và bugi.
    • Khi bạn không thể khởi tạo sự phụ thuộc trước khi pass chúng vào – cho ví dụ, khi sử dụng khởi tạo lazy hoặc xác định phạm vi đối tượng cho các luồng ứng dụng của bạn, bạn cần viết và maintain một vùng chứa tùy chỉnh( hoặc graph của Dependencies ) cái mà quản lý lifetimess của các sự phụ thuộc của bạn trong memory.

    Có các thư viện giải quyết vấn đề này bằng cách tự động tiến hành tạo và cung cấp các sư phụ thuộc. Chúng phù hợp với hai loại:

    • Reflection-based, giải pháp kết nối các sự phụ thuộc tại runtime.
    • Static, giải pháp tạo code để kết nối các sự phụ thuộc tại compile time.

    Dragger là một thư viện DI phổ biến cho java, kotlin và Android do Google maintain. Dragger tạo điều kiện thuận lợi cho việc sử dụng DI trong ứng dụng của bạn bằng cách tạo và quản lý graph dependencies cho bạn. Nó cung cấp cá phụ thuộc hoàn toàn static và compiletime giải quyết nhiều vấn đề về phát triển và hiêu xuấn của các giải pháp sự trên phản xạ.

    Hilt là một thư viện được đề xuất của Jetpack để chèn phụ thuộc trong Android.

    Kết luận

    Dependence injection cung cấp cho ứng dụng của bạn những ưu điểm sau:

    • Khả năng tái sử dụng của các class, việc hoán đổi các implementation dễ dàng hơn.
    • Dễ tái cấu trúc: Các phần phụ thuộc có thể kiểm tra tại thời điểm tạo đối tượng hoặc compile time thay vì bị ẩn dưới dạng chi tiết triển khai.
    • Dễ test: Một class không quản lý các phụ thuộc của nó, vì vậy ta có thể truyền các implementation khác nhau để kiểm tra các trường hợp khác nhau của mình.
  • Inter-Process Communication with android (phần 2)

    Inter-Process Communication with android (phần 2)

    Ở phần trước mình đã giới thiệu về IPC và sử dụng IPC thông qua Messenger.

    Phần 1: https://magz.techover.io/2021/12/19/inter-process-communication-with-android-phan-1/

    Còn bây giờ mình sẽ sự dung IPC thông qua AIDL.

    Content

    1. AIDL là gì?
    2. Tạo .aidl
    3. Demo với aidl
    4. Kết luận

    1. AIDL là gì?

    AIDL hay Android Interface Definition Language là một cách cho phép bạn có thể định nghĩa một cách mà cả client và server (1 ứng dụng đóng vai trò là server cho các ứng dụng khác đóng vai trò là client có thể truy cập tới) có thể giao tiếng với nhau thông qua truyền thông liên tiến trình Interprocess communication (IPC). Thông thường, trong Android một process (tiến trình) không thể trực tiếp truy cập vào bộ nhớ của một tiến trình khác. Vì vậy để có thể các tiến trình có thể giao tiếp với nhau, chúng cần phân tách các đối tượng thành dạng đối tượng nguyên thủy (primitive object) mà hệ thống có thể hiểu được.

    • Lưu ý Chỉ nên sử AIDL nếu bạn muốn các ứng dụng khác có thể kết nối tới service của ứng dụng bạn thông qua truyền thông liên tiến trinh. Còn nếu bạn chỉ đơn thuần muốn sử dụng service trong ứng dụng của mình bạn nên sử dụng Binder.

    2. Tạo .aidl file

    AIDL sử dụng cú pháp rất đơn giản cho phép bạn khai báo một interface với một hoặc nhiều phuơng thước có thể có một hoặc nhiều tham số và trả về một giá trị. Các tham số và giá trị trả về có thể thuộc bất kì loại nào thậm chí các interface hoặc object được tạo ra bởi các AIDL khác.

    Bạn cần xây dựng một .aidl file sử dụng ngôn ngữ Java, mỗi một .aidl file cần định nghĩa duy nhất một interface.

    Mặc định AIDL hộ trỡ các kiểu dữ liệu sau:

    • 8 kiểu dữ liệu nguyên thủy trong java (int, byte, short, long, float, double, boolean, char).
    • String.
    • CharSequence.
    • List, Map Tất cả các phần tử trong List, Map phải là một trong những loại dữ liệu được hỗ trợ trong danh sách này hoặc một trong các đối tượng được định nghĩa thông qua một aidl interface khác.

    Khi định nghĩa aidl interface bạn cần lưu ý:

    • Phuơng thưsc có thể nhận vào 0 hoặc nhiều parameter và trả về void hoặc một giá trị cụ thể.
    • aidl chỉ hỗ trợ các phương thức không hỗ trợ các static field.

    Để tạo file aidl các bạn sử dụng AS làm như sau:

    Đây là một tệp .aidl ví dụ:

    // IRemoteService.aidl
    package com.example.android;
    
    // Declare any non-default types here with import statements
    
    /** Example service interface */
    interface IRemoteService {
        /** Request the process ID of this service, to do evil things with it. */
        int getPid();
    
        /** Demonstrates some basic types that you can use as parameters
         * and return values in AIDL.
         */
        void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
                double aDouble, String aString);
    }

    Lưu ý sau khi viết xong file aidl các bạn cần rebuild lại project để SDK tool gen ra file java để có thể sử dụng.

    3. Demo với AIDL

    Client

    Tạo tệp .aidl:

    // IIPCExample.aidl
    package com.pmirkelam.ipcserver;
    
    // Declare any non-default types here with import statements
    
    interface IIPCExample {
        /** Request the process ID of this service */
        int getPid();
    
        /** Count of received connection requests from clients */
        int getConnectionCount();
    
        /** Set displayed value of screen */
        void setDisplayedValue(String packageName, int pid, String data);
    }

    fragment_aidl.xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context=".ui.MainActivity">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/say_something_to_server"
            android:textSize="25sp" />
    
        <EditText
            android:id="@+id/edt_client_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/salut"
            android:inputType="text"
            android:textAlignment="center"
            android:textSize="35sp" />
    
        <Button
            android:id="@+id/btn_connect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/connect"
            android:textSize="18sp" />
    
        <LinearLayout
            android:id="@+id/linear_layout_client_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="invisible">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/process_id_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_pid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="30dp"
                android:text="@string/connection_count_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_connection_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
        </LinearLayout>
    </LinearLayout>

    AIDLFragmet:

    class AidlFragment : Fragment(), ServiceConnection, View.OnClickListener {
    
        private var _binding: FragmentAidlBinding? = null
        private val binding get() = _binding!!
        var iRemoteService: IIPCExample? = null
        private var connected = false
    
        override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
        ): View? {
            _binding = FragmentAidlBinding.inflate(inflater, container, false)
            val view = binding.root
            return view
        }
    
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            binding.btnConnect.setOnClickListener(this)
        }
    
        override fun onDestroyView() {
            super.onDestroyView()
            _binding = null
        }
    
        override fun onClick(v: View?) {
                connected = if (connected) {
                    disconnectToRemoteService()
                    binding.txtServerPid.text = ""
                    binding.txtServerConnectionCount.text = ""
                    binding.btnConnect.text = getString(R.string.connect)
                    binding.linearLayoutClientInfo.visibility = View.INVISIBLE
                    false
                } else {
                    connectToRemoteService()
                    binding.linearLayoutClientInfo.visibility = View.VISIBLE
                    binding.btnConnect.text = getString(R.string.disconnect)
                    true
                }
        }
        private fun connectToRemoteService() {
            val intent = Intent("aidlexample")
            val pack = IIPCExample::class.java.`package`
            pack?.let {
                intent.setPackage(pack.name)
                activity?.applicationContext?.bindService(
                    intent, this, Context.BIND_AUTO_CREATE
                )
            }
        }
    
        private fun disconnectToRemoteService() {
            if(connected){
                activity?.applicationContext?.unbindService(this)
            }
        }
    
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            // Gets an instance of the AIDL interface named IIPCExample,
            // which we can use to call on the service
            iRemoteService = IIPCExample.Stub.asInterface(service)
            binding.txtServerPid.text = iRemoteService?.pid.toString()
            binding.txtServerConnectionCount.text = iRemoteService?.connectionCount.toString()
            iRemoteService?.setDisplayedValue(
                context?.packageName,
                Process.myPid(),
                binding.edtClientData.text.toString())
            connected = true
        }
    
        override fun onServiceDisconnected(name: ComponentName?) {
            Toast.makeText(context, "IPC server has disconnected unexpectedly", Toast.LENGTH_LONG).show()
            iRemoteService = null
            connected = false
        }
    }

    main_activity.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="?attr/actionBarSize">
    
        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/nav_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/mobile_navigation" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    Server

    Tạo 1 tệp .aidl giống Client:

    activity_main.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/connection_status"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/no_connected_client"
            android:textSize="30sp"
            android:textStyle="bold" />
    
        <LinearLayout
            android:id="@+id/linear_layout_client_state"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="75dp"
                android:text="@string/package_name"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_package_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="com.someone.something" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/pid"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_pid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="21282" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/data"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_data"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="Salut!" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/ipc_method"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_ipc_method"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="30sp"
                android:textStyle="bold"
                tools:text="Something" />
        </LinearLayout>
    
    </LinearLayout>

    Client.kt:

    data class Client(
        var clientPackageName: String?,
        var clientProcessId: String?,
        var clientData: String?,
        var ipcMethod: String
    )
    
    object RecentClient {
        var client: Client? = null
    }
    • Tạo lớp dữ liệu khách hàng.
    • Tạo một lớp RecentClient singleton nơi chúng tôi sẽ giữ máy khách được kết nối cuối cùng.

    Constant.kt:

    Đây là dữ liệu dùng chung.

    // Bundle keys
    const val PID = "pid"
    const val CONNECTION_COUNT = "connection_count"
    const val PACKAGE_NAME = "package_name"
    const val DATA = "data"

    AidlFragment.kt:

    class IPCServerService : Service() {
    
        companion object {
            // How many connection requests have been received since the service started
            var connectionCount: Int = 0
    
            // Client might have sent an empty data
            const val NOT_SENT = "Not sent!"
        }
    
        // AIDL IPC - Binder object to pass to the client
        private val aidlBinder = object : IIPCExample.Stub() {
    
            override fun getPid(): Int = Process.myPid()
    
            override fun getConnectionCount(): Int = IPCServerService.connectionCount
    
            override fun setDisplayedValue(packageName: String?, pid: Int, data: String?) {
                val clientData =
                    if (data == null || TextUtils.isEmpty(data)) NOT_SENT
                    else data
    
                // Get message from client. Save recent connected client info.
                RecentClient.client = Client(
                    packageName ?: NOT_SENT,
                    pid.toString(),
                    clientData,
                    "AIDL"
                )
            }
        }
    
        // Pass the binder object to clients so they can communicate with this service
        override fun onBind(intent: Intent?): IBinder? {
            connectionCount++
            // Choose which binder we need to return based on the type of IPC the client makes
            return when (intent?.action) {
                "aidlexample" -> aidlBinder
                else -> null
            }
        }
    
        // A client has unbound from the service
        override fun onUnbind(intent: Intent?): Boolean {
            RecentClient.client = null
            return super.onUnbind(intent)
        }
    
    }
    • Tạo đối tượng chất kết dính của chúng tôi bằng cách mở rộng từ lớp Stub. Ở đây chúng tôi điền các phương thức.
    • Trả lại đối tượng kết dính của chúng tôi cho khách hàng với ràng buộc.
    • Cập nhật đối tượng RecentClient của chúng tôi khi máy khách liên kết và hủy liên kết để giao diện người dùng được cập nhật.

    Trong file Manifest:

    <service
                android:name=".IPCServerService">
                <intent-filter>
                    <action android:name="aidlexample" />
                </intent-filter>
            </service>

    MainActivity.kt:

    class MainActivity : AppCompatActivity() {
    
        private lateinit var binding: ActivityMainBinding
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
        }
    
        override fun onStart() {
            super.onStart()
            val client = RecentClient.client
            binding.connectionStatus.text =
                if (client == null) {
                    binding.linearLayoutClientState.visibility = View.INVISIBLE
                    getString(R.string.no_connected_client)
                } else {
                    binding.linearLayoutClientState.visibility = View.VISIBLE
                    getString(R.string.last_connected_client_info)
                }
            binding.txtPackageName.text = client?.clientPackageName
            binding.txtServerPid.text = client?.clientProcessId
            binding.txtData.text = client?.clientData
            binding.txtIpcMethod.text = client?.ipcMethod
        }
    }
    • Bây giờ chúng tôi có thể cập nhật Hoạt động theo thông tin khách hàng.
    • Máy khách được kết nối cuối cùng sẽ được hiển thị. Bố cục sẽ bị ẩn nếu không có ứng dụng khách được kết nối.

    Chú ý: Hầu hết các ứng dụng không nên sử dụng AIDL để tạo một bound service, bởi vì nó có thể yêu cầu khả năng đa luồng và có thể dẫn đến việc triển khai phức tạp hơn. Như vậy, AIDL không phù hợp với hầu hết các ứng dụng nên ta sẽ không thảo luận về cách sử dụng nó.

    4. Kết Luận

    Trên Đây là cách đơn giản nhất để thực hiện IPC bằng AIDL.

    Cảm ơn mọi người đã đọc bài viết.

  • Inter-Process Communication with android (phần 1)

    Inter-Process Communication with android (phần 1)

    Content

    1. Inter-Process Communication(ipc) là gì?
    2. Tại sao cần giao tiếp?
    3. Các mô hình IPC
    4. IPC trong Android
    5. Sử dụng Messenger
    6. Demo với Messenger
    7. Kết luận

    1. Inter-Process Communication(ipc) là gì?

    Inter-process communication (IPC) là một cơ chế cho phép các quá trình giao tiếp với nhau và đồng bộ hóa các hành động của chúng.

    Thông thường, các ứng dụng có thể sử dụng IPC, được phân loại là máy khách và máy chủ, trong đó máy khách (client) yêu cầu dữ liệu và máy chủ (server) đáp ứng yêu cầu của máy khách.

    2. Tại sao cần giao tiếp?

    • Chia sẻ dữ liệu giữa 2 người dùng.
    • Mô-đun riêng biệt.
    • Thực thi nhiều quy trình dễ dàng hơn.

    3. Các mô hình IPC

    Đây là 2 mô hình IPC mình biết:

    • Share Memory: Giao tiếp giữa các tiến trình sử dụng bộ nhớ dùng chung yêu cầu các tiến trình phải chia sẻ một số biến và nó hoàn toàn phụ thuộc vào cách lập trình viên sẽ thực hiện nó.
    • Message passing: Các quá trình giao tiếp với nhau mà không cần sử dụng bất kỳ loại bộ nhớ dùng chung nào. Hai tiến trình p1 và p2 muốn giao tiếp với nhau, chúng tiến hành như sau:
      1. Thiết lập giao tiếp.
      2. Bắt đầu trao đổi tin nhắn bằng cách sử dụng cơ bản primitives.
        • send(message, destinaion) or send(message)
        • receive(message, host) or receive(message)

    4. IPC trong Android

    Thường có hai cách để triển khai IPC trong Android:

    • Using a Messenger: Thực hiện giao tiếp giữa các quá trình (IPC) với các ứng dụng khác bằng đối tượng Messenger và cho phép dịch vụ xử lý một cuộc gọi tại một thời điểm.
    • Using AIDL: Tạo tệp .aidl xác định IPC để cho phép máy khách từ các ứng dụng khác nhau truy cập dịch vụ và xử lý đa luồng đến dịch vụ.

    5. Sử dụng Messenger

    Messenger là một Trình xử lý được gửi đến quy trình từ xa.

    Các bước triển khai IPC bằng Messenger:

    1. Service implements mmột Handler nhận một cuộc gọi lại cho mỗi cuộc gọi từ một máy khách.
    2. Service sử dụng Handler để tạo một đối tượng Messenger (là một tham chiếu đến Handler).
    3. Messenger tạo IBinder mà dịch vụ trả về cho khách hàng từ onBind().
    4. Clients sử dụng IBinder để khởi tạo Messenger (tham chiếu đến Handler của Service), mà client sử dụng để gửi các đối tượng Message tới service.
    5. Service từng Message trong Handler của nó — cụ thể là trong phương thức handleMessage().

    6. Demo với Messenger

    Server

    Bắt đầu với ứng dụng Server.

    Khai báo trong Manifest:

    <service android:name=".IPCServerService">
                <intent-filter>
                    <action android:name="messengerexample" />
                </intent-filter>
            </service>
    // Messenger IPC - Messenger object contains binder to send to client
        private val mMessenger = Messenger(IncomingHandler())
    
        // Messenger IPC - Message Handler
        internal inner class IncomingHandler : Handler() {
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                // Get message from client. Save recent connected client info.
                val receivedBundle = msg.data
                RecentClient.client = Client(
                    receivedBundle.getString(PACKAGE_NAME),
                    receivedBundle.getInt(PID).toString(),
                    receivedBundle.getString(DATA),
                    "Messenger"
                )
    
                // Send message to the client. The message contains server info
                val message = Message.obtain(this@IncomingHandler, 0)
                val bundle = Bundle()
                bundle.putInt(CONNECTION_COUNT, connectionCount)
                bundle.putInt(PID, Process.myPid())
                message.data = bundle
                // The service can save the msg.replyTo object as a local variable
                // so that it can send a message to the client at any time
                msg.replyTo.send(message)
            }
        }
    • Tạo một đối tượng Handler trong dịch vụ để xử lý các thông báo từ client.
    • Cập nhật đối tượng RecentClient để cập nhật GUI theo thông báo đến.
    • Hãy tạo Messenger với đối tượng Handler. Giao tiếp sẽ diễn ra thông qua đối tượng này.
    • Các msg.replyTo đối tượng được lấy từ thông điệp của khách hàng.

    Các hằng số chúng tôi sử dụng làm khóa gói trong thư mục:

    package com.pmirkelam.ipcserver
    // Bundle keys
    const val PID = "pid"
    const val CONNECTION_COUNT = "connection_count"
    const val PACKAGE_NAME = "package_name"
    const val DATA = "data"

    Trả lại cho client:

    // Pass the binder object to clients so they can communicate with this service
        override fun onBind(intent: Intent?): IBinder? {
            connectionCount++
            // Choose which binder we need to return based on the type of IPC the client makes
            return when (intent?.action) {
                "aidlexample" -> aidlBinder
                "messengerexample" -> mMessenger.binder
                else -> null
            }
        }

    Client

    Hãy thêm các hằng số mà tôi sử dụng làm khóa gói ở đây:

    package com.pmirkelam.ipcclient
    
    // Bundle keys
    const val PID = "pid"
    const val CONNECTION_COUNT = "connection_count"
    const val PACKAGE_NAME = "package_name"
    const val DATA = "data"

    Trong tệp xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context=".ui.MainActivity">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@string/say_something_to_server"
            android:textSize="25sp" />
    
        <EditText
            android:id="@+id/edt_client_data"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/salut"
            android:inputType="text"
            android:textAlignment="center"
            android:textSize="35sp" />
    
        <Button
            android:id="@+id/btn_connect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="Connect via Messenger"
            android:textSize="18sp" />
    
        <LinearLayout
            android:id="@+id/linear_layout_client_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="invisible">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="50dp"
                android:text="@string/process_id_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_pid"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="30dp"
                android:text="@string/connection_count_of_server"
                android:textSize="25sp" />
    
            <TextView
                android:id="@+id/txt_server_connection_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="50sp" />
        </LinearLayout>
    </LinearLayout>

    Trong tệp code:

    class MessengerFragment : Fragment(), ServiceConnection, View.OnClickListener {
    
        private var _binding: FragmentMessengerBinding? = null
        private val viewBinding get() = _binding!!
    
        // Is bound to the service of remote process
        private var isBound: Boolean = false
    
        // Messenger on the server
        private var serverMessenger: Messenger? = null
    
        // Messenger on the client
        private var clientMessenger: Messenger? = null
    
        // Handle messages from the remote service
        var handler: Handler = object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(msg: Message) {
    		    // Update UI with remote process info
                val bundle = msg.data
                viewBinding.linearLayoutClientInfo.visibility = View.VISIBLE
                viewBinding.btnConnect.text = getString(R.string.disconnect)
                viewBinding.txtServerPid.text = bundle.getInt(PID).toString()
                viewBinding.txtServerConnectionCount.text =
                bundle.getInt(CONNECTION_COUNT).toString()
            }
        }
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            _binding = FragmentMessengerBinding.inflate(inflater, container, false)
            return viewBinding.root
        }
    
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            viewBinding.btnConnect.setOnClickListener(this)
        }
    
        override fun onClick(v: View?) {
            if(isBound){
                doUnbindService()
            } else {
                doBindService()
            }
        }
    
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            serverMessenger = Messenger(service)
            // Ready to send messages to remote service
            sendMessageToServer()
        }
    
        override fun onServiceDisconnected(className: ComponentName) {
            clearUI()
            serverMessenger = null
        }
    
        private fun clearUI(){
            viewBinding.txtServerPid.text = ""
            viewBinding.txtServerConnectionCount.text = ""
            viewBinding.btnConnect.text = getString(R.string.connect)
            viewBinding.linearLayoutClientInfo.visibility = View.INVISIBLE
        }
    
        override fun onDestroy() {
            doUnbindService()
            super.onDestroy()
        }
    
        private fun doBindService() {
            clientMessenger = Messenger(handler)
            Intent("messengerexample").also { intent ->
                intent.`package` = "com.pmirkelam.ipcserver"
                activity?.applicationContext?.bindService(intent, this, Context.BIND_AUTO_CREATE)
            }
            isBound = true
        }
    
        private fun doUnbindService() {
            if (isBound) {
                activity?.applicationContext?.unbindService(this)
                isBound = false
            }
        }
    
        private fun sendMessageToServer() {
            if (!isBound) return
            val message = Message.obtain(handler)
            val bundle = Bundle()
            bundle.putString(DATA, viewBinding.edtClientData.text.toString())
            bundle.putString(PACKAGE_NAME, context?.packageName)
            bundle.putInt(PID, Process.myPid())
            message.data = bundle
            message.replyTo = clientMessenger // we offer our Messenger object for communication to be two-way
            try {
                serverMessenger?.send(message)
            } catch (e: RemoteException) {
                e.printStackTrace()
            } finally {
                message.recycle()
            }
        }
    }

    7. Kết Luận

    Trên Đây là cách đơn giản nhất để thực hiện IPC, bởi vì Messenger xếp hàng tất cả các requests vào một luồng đơn để bạn không phải thiết kế service của mình là an toàn luồng.

    Ở phần tiếp theo mình sẽ nói tiếp cách sử dụng ADIL trong IPC.

    Cảm ơn mọi người đã đọc bài viết.

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

    Giới thiệu về Transfer learning

    Mở đầu

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

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

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

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

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

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

    Introduction

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Transfer learning workflow

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

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

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

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

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

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

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

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

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

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

    base_model.trainable = False
    

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

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

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

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

    Kết luận

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

    Thanks for reading

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

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

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

    Xin chào mọi người, ở bài viết trước thì mình đã giới thiệu qua về Hệ thống gợi ý ( Recommendation Systems ) và trong bài biết lần này mình sẽ trình bày với các bạn 1 phương pháp của Lọc cộng tác ( Collaborative Filtering ) có tên là Neighborhood-based Collaborative Filtering (NBCF)

    Nội dung trong phần này

    Giới thiệu

    Trong Content-based Recommendation Systems, chúng ta đã biết đặc điểm của Content-based Recommendation Systems là việc xây dựng mô hình cho mỗi user không phụ thuộc vào các users khác mà phụ thuộc vào profile của mỗi items. Việc làm này có lợi thế là tiết kiệm bộ nhớ và thời gian tính toán. Đồng thời, hệ thống có khả năng tận dụng các thông tin đặc trưng của mỗi item như được mô tả trong bản mô tả (description) của mỗi item. Bản mô tả này có thể được xây dựng bởi nhà cung cấp hoặc được thu thập bằng cách yêu cầu users gắn tags cho items. Việc xây dựng feature vector cho mỗi item thường bao gồm các kỹ thuật Xử lý ngôn ngữ tự nhiên (Natural Language Processing – NLP).

    Cách làm trên có hai nhược điểm cơ bản. Thứ nhất, khi xây dựng mô hình cho một user, các hệ thống Content-based không tận dụng được thông tin từ các users khác. Những thông tin này thường rất hữu ích vì hành vi mua hàng của các users thường được nhóm thành một vài nhóm đơn giản; nếu biết hành vi mua hàng của một vài users trong nhóm, hệ thống nên suy luận ra hành vi của những users còn lại. Thứ hai, không phải lúc nào chúng ta cũng có bản mô tả cho mỗi item. Việc yêu cầu users gắn tags còn khó khăn hơn vì không phải ai cũng sẵn sàng làm việc đó; hoặc có làm nhưng sẽ mang xu hướng cá nhân. Các thuật toán NLP cũng phức tạp hơn ở việc phải xử lý các từ gần nghĩa, viết tắt, sai chính tả, hoặc được viết ở các ngôn ngữ khác nhau.

    Những nhược điểm phía trên có thể được giải quyết bằng Collaborative Filtering (CF). Trong bài viết này, tôi sẽ trình bày tới các bạn một phương pháp CF có tên là Neighborhood-based Collaborative Filtering (NBCF). Bài tiếp theo sẽ trình bày về một phương pháp CF khác có tên Matrix Factorization Collaborative Filtering. Khi chỉ nói Collaborative Filtering, chúng ta sẽ ngầm hiểu rằng phương pháp được sử dụng là Neighborhood-based.

    Ý tưởng cơ bản của NBCF là xác định mức độ quan tâm của một user tới một item dựa trên các users khác gần giống với user này. Việc gần giống nhau giữa các users có thể được xác định thông qua mức độ quan tâm của các users này tới các items khác mà hệ thống đã biết. Ví dụ, A, B đều thích phim Cảnh sát hình sự, tức đều rate bộ phim này 5 sao. Ta đã biết A cũng thích Người phán xử, vậy nhiều khả năng B cũng thích bộ phim này.

    Các bạn có thể đã hình dung ra, hai câu hỏi quan trọng nhất trong một hệ thống Neighborhood-based Collaborative Filtering là:

    • Làm thế nào xác định được sự giống nhau giữa hai users?
    • Khi đã xác định được các users gần giống nhau (similar users) rồi, làm thế nào dự đoán được mức độ quan tâm của một user lên một item?

    Việc xác định mức độ quan tâm của mỗi user tới một item dựa trên mức độ quan tâm của similar users tới item đó còn được gọi là User-user collaborative filtering. Có một hướng tiếp cận khác được cho là làm việc hiệu quả hơn là Item-item collaborative filtering. Trong hướng tiếp cận này, thay vì xác định user similarities, hệ thống sẽ xác định item similarities. Từ đó, hệ thống gợi ý những items gần giống với những items mà user có mức độ quan tâm cao.

    Phương pháp này được chia thành 2 hướng nhỏ, là User-User Collaborative Filtering (uuCF) và Item-Item Collaborative Filtering (iiCF).

    uuCF : Về cơ bản,anh em có thể hiểu là tìm ra những nhóm User tương tự nhau từ đó sẽ đưa ra dự đoán mực độ quan tâm của User trên các User khác cũng nhóm . Về chi tiết mình sẽ nói ỏ phần 2

    iiCF : Tương tự như User-User, phương pháp này sẽ tìm ra những nhóm item tương tự nhau. Sau đó, dự đoán mức độ yêu thích của user với item dựa trên độ yêu thích của user đó với các item khác cùng loại. Về chi tiết mình sẽ nói ở phần 3

    User-user Collaborative Filtering

    Đầu vào của bài toán là Utility matrix và công việc quan trọng nhất trong uuCF là phải xác định được sự giống nhau giữa hai User và cách xác định sự giống nhau này dựa trên độ quan tâm với các item giữa các User. Ví dụ nhé:

    Hình trên là 1 ví dụ về đầu vào là utility matrix dựa trên số sao rating của các user với item. Nhìn phát thấy luôn là User 1 và 0 cho rating về các item khá giống nhau ít ra còn giống hơn mấy các User còn lại vậy nên ta có thể đoàn rằng user 0 cũng quan tâm tới item 2 như user 1

    Vậy để thuận tiện tính toán similarity giữa các User ta phải điền giá trị vào các ô ‘?’. Vậy mỗi ô ‘?’ cần được điền giá trị như thế nào để thích hợp, có thể nhiều người sẽ nghĩ đến là 0 nhưng điều này mình đánh giá sẽ ảnh hưởng khá lớn trong việc đưa ra gợi ý vì mức 0 có nghĩa đây là 1 mức khá tiêu cực đối với item. Một giá trị mình thấy rất khả quan hơn trong trường hợp này chính là giá trị trung bình cộng của các ratings mà User đã đánh giá cho các item còn lại. Ví dụ như ở user đầu ta có thể tính được giá trị trung bình này là 3.25 được tính từ trung bình cộng của 5-3-2-2. Ta sẽ tính được utility matrix

    Tiếp theo chúng ta sẽ xây dựng normalized utility matrix bằng cách trừ mỗi rating đi với giá trị trung bình. Vậy tại sao lại cần bước chuẩn hóa này lại cần thiết và quan trọng như vâỵ thì rất dễ hiểu thôi:

    • Việc trừ đi trung bình cộng của mỗi cột khiến trong trong mỗi cột có những giá trị dương và âm và 0 ( là giá trị chưa xác định ) . Những giá trị âm và dương này đại diện cho sự quan tâm của User đến Item là thích hoặc không thích còn 0 là đại diện cho những User không có chứng kiến =)) hiểu nôm na là chưa xác định được có thích hay không.
    • Về mặt kỹ thuật, số chiều của utility matrix là rất lớn với hàng triệu users và items, nếu lưu toàn bộ các giá trị này trong một ma trận thì khả năng cao là sẽ không đủ bộ nhớ. Quan sát thấy rằng vì số lượng ratings biết trước thường là một số rất nhỏ so với kích thước của utility matrix, sẽ tốt hơn nếu chúng ta lưu ma trận này dưới dạng sparse matrix, tức chỉ lưu các giá trị khác không và vị trí của chúng. Vì vậy, tốt hơn hết, các dấu ‘?’ nên được thay bằng giá trị ‘0’, tức chưa xác định liệu user có thích item hay không. Việc này không những tối ưu bộ nhớ mà việc tính toán similarity matrix sau này cũng hiệu quả hơn.

    Ma trận sau khi được chuẩn hóa còn được gọi là normalized utility matrix như hình dưới:

    Tính toán Similarity

    vậy sau khi xây dựng xong normalized utility matrix , bước quan trọng tiếp theo chúng cần cần làm là tính toán độ tương đồng giữa các User.

    Ta gọi mực độ tương đồng của 2 User là Sim(Ui,Uj). Để xây dựng 1 hàm Similarity thì chúng ta cần đảm bảo yếu tố sau :

    Ở đây mình sẽ đề cập đến 2 similarity function đó là

    • Cosine Similarity : Đây là hàm được sử dụng nhiều nhất, và cũng quen thuộc với các bạn nhất. Nếu các bạn không nhớ công thức tính cos của góc giữa hai vector u1 , u2 trong chương trình phổ thông, thì dưới đây là công thức:

    Trong bài này mình sẽ sử dụng công thức này nhé.

    • Pearson Corelation : Mình thường k hay sử dụng công thức này , anh em có thể tham khảo thêm ở đây

    Sau khi sử dụng Cosine Similarity thì chúng ta sẽ thu được ma trận Similarity như sau:

    Rating Prediction

    Sau khi xây dựng được ma trận Similarity thì chúng ta phải đưa ra dự đoán đúng không nào? Ở đây chúng ta sẽ dự đoán theo K- User gần nhất . K càng nhiều thì dự đoán sẽ càng chính xác nghe khá giống với K-NN nhỉ.

    Công thức được đưa ra ở đây là:

    Để mình lấy ví dụ minh họa dự đoán user 5 rating cho item 3 với K = 3 luôn cho anh em dễ hiểu nhé:

    • Xác định các User đã rated cho item 3 là User 0,1,2,3,4,6
    • Xác định similarities của User 5 với các User còn lại lần lượt là : 0.2 , -0.23 , 0.47 , -0.29 , 0 , 0.56 với K = 3 thì ta chọn 0,2,6
    • Xác định Normalized ratings của User 0,2,6 lần lượt là -1.25 , 0.5 , 0.67
    • Đưa ra dự đoán theo công thức :

    Thực hiện tương tự là ta có thể hoàn thiện normalized ratings matrix :

    Đến bước này là gần như đã xong việc cuối cùng của ae là cộng lại với gái trị trung bình ban đầu tính được là ta có thể đưa ra dự đoán của 1 User với 1 Item rồi

    Item-item Collaborative Filtering

    Một số hạn chês của User-user CF:

    • Trên thực tế, số lượng users luôn lớn hơn số lượng items rất nhiều. Kéo theo đó là Similarity matrix là rất lớn với số phần tử phải lưu giữ là hơn 1 nửa của bình phương số lượng users (chú ý rằng ma trận này là đối xứng). Việc này, như đã đề cập ở trên, khiến cho việc lưu trữ ma trận này trong nhiều trường hợp là không khả thi.
    • Ma trận Utility Y thường là rất sparse. Với số lượng users rất lớn so với số lượng items, rất nhiều cột của ma trận này sẽ rất sparse, tức chỉ có một vài phần tử khác 0. Lý do là users thường lười rating. Cũng chính vì việc này, một khi user đó thay đổi rating hoặc rate thêm items, trung bình cộng các ratings cũng như vector chuẩn hoá tương ứng với user này thay đổi nhiều. Kéo theo đó, việc tính toán ma trận Similarity, vốn tốn nhiều bộ nhớ và thời gian, cũng cần được thực hiện lại.

    Ngược lại, nếu chúng ta tính toán similarity giữa các items rồi recommend những items gần giống với item yêu thích của một user thì sẽ có những lợi ích sau:

    • Vì số lượng items thường nhỏ hơn số lượng users, Similarity matrix trong trường hợp này cũng nhỏ hơn nhiều, thuận lợi cho việc lưu trữ và tính toán ở các bước sau.
    • Vì số lượng phần tử đã biết trong Utility matrix là như nhau nhưng số hàng (items) ít hơn số cột (users), nên trung bình, mỗi hàng của ma trận này sẽ có nhiều phần tử đã biết hơn số phần tử đã biết trong mỗi cột. Việc này cũng dễ hiểu vì mỗi item có thể được rated bởi nhiều users. Kéo theo đó, giá trị trung bình của mỗi hàng ít bị thay đổi hơn khi có thêm một vài ratings. Như vậy, việc cập nhật ma trận Similarity Matrix có thể được thực hiện ít thường xuyên hơn.

    Cách tiếp cận thứ hai này được gọi là Item-item Collaborative Filtering. Mình thấy hướng tiếp cận này được sử dụng nhiều trong thực tế hơn.

    Quy trình dự đoán missing ratings cũng tương tự như trong User-user CF

    Đầu tiên tính giá trị trung bình của rating theo item:

    Xây dựng normalized utility matrix :

    Xây dựng Item similarity matrix :

    Đưa ra dự đoán còn thiếu:

    Trên đây là lý thuyết về NBCF. Mình xin kết thúc bài viết này ở đây , ở phần sau mình sẽ đi vào code Python và kiểm thử dữ liệu cho anh em nhé . Hẹn ae ở bài viết tới

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

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

    2. Kho Dataset cho anh em GroupLens

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

  • MVVM with Android

    MVVM with Android

    Ở bài viết trước mình có nó qua của hạn chế chủa MVP so với MVVM. Nên ở bài viết này mình sẽ tìm hiểu kĩ về mô hình MVVM để biết lý do chọn MVVM hơn là MVP.

    Content

    • Liva Data là gì?
    • DataBinding là gì?
    • MVVM là gì?
    • Demo MVVM
    • Kết Luận

    Live Data là gì?

    LiveData là observable data holder class. Không giống như các observables khác, nó nhận thức được vòng đời, tức là nhận nhận thức được vòng đời của các components như là activity, fragment hay services. Điều này có nghĩa là nó chỉ cập nhật các component observers khi chúng ở trạng thái vòng đời hoạt động.

    DataBinding là gì?

    Là library hỗ trợ cho phép chúng ta liên kết với các thành phần UI của mình từ layout với data source trong ứng dụng của chúng ta bằng cách khai báo thay vì lập trình.

    Set up:

    android {
      ...
      dataBinding {
        enabled = true
      }
    }

    MVVM là gì?

    Mô hình MVVM

    MVVM gồm 3 phần:

    Model: Cũng tương tự như trong mô hình MVC. Model là các đối tượng giúp truy xuất và thao tác trên dữ liệu thực sự.

    View: là thành phần UI đại diện cho trạng thái hiện tại cảu thông tin mà người dùng có thể nhìn thấy.

    ViewModel:

    • ViewModel nằm giữa Model View, có tác dụng là cầu nối để giao tiếp giữa Model View.
    • ViewModel còn có tác dụng xử lí các logic convert, format data trước khi View hiển thị data đó cho người dùng.
    • Đây là phần khác biệt của MVVM so với MVP.
    • ViewModel và View được kết thông thông qua Databiding và observable Livedata,

    Demo MVVM

    Model layer:

    Ví dụ về cách triển khai 1 Model:

    data class User(
        var username: String,
        var password: String
    )

    ViewModel layer:

    Ví dụ về cách triển khai 1 ViewModel:

    class LoginViewModel(
        private val userRepository: IUserRepository,
        private val dispatcher: CoroutineDispatcher = Dispatchers.Main
    ) : ViewModel() {
    
        private val _loginResult = MutableLiveData<Resource<Status>>()
        val loginResult: LiveData<Resource<Status>>
            get() = _loginResult
    
        private val _stateLoading = MutableLiveData<Resource<Boolean>>()
        val stateLoading: LiveData<Resource<Boolean>>
            get() = _stateLoading
    
        private var listDataUser = listOf<User>()
    
        fun clickLogin(inputUser: User) {
            viewModelScope.launch(dispatcher) {
                listDataUser = userRepository.getUserList()
                _stateLoading.value = Resource.loading(true)
                val invalid = checkInvalidInput(inputUser)
                if (invalid) {
                    _loginResult.postValue(Resource.invalid(Status.INVALID, Status.INVALID.value))
                    _stateLoading.value = Resource.loading(false)
                } else {
                    checkLogin(inputUser)
                }
            }
        }
    
        private suspend fun checkInvalidInput(inputUser: User) =
            userRepository.checkInvalidInput(inputUser)
    
        private suspend fun checkLogin(inputUser: User) {
            val isSuccess = userRepository.loginResult(inputUser, listDataUser)
            if (isSuccess) {
                _loginResult.postValue(Resource.success(Status.SUCCESS, Status.SUCCESS.value))
            } else {
                _loginResult.postValue(Resource.error(Status.ERROR, Status.ERROR.value))
            }
            _stateLoading.value = Resource.loading(false)
        }
    
        class Factory : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return LoginViewModel(Injector.userRepository) as T
            }
        }
    }
    1. Thuộc tính userRepository để lấy dữ liệu từ data.
    2. Hàm checkInvalidInput dùng để check dữ liệu nhập vào null hay không.
    3. Hàm checkLogin check dữ liệu nhập vào có trùng với data không.
    4. Thuộc tính _loginResult trả về Success nếu đúng dữ liệu, Error là sai còn Invalid là dữ liệu null.

    View layer:

    Ví dụ về cách triển khai 1 View:

    Code file .xml:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical">
    
            <TextView
                android:id="@+id/tvLogin"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginBottom="36dp"
                android:text="@string/login_text_view"
                android:textSize="@dimen/login_text_view_size" />
    
            <EditText
                android:id="@+id/edtUserName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:drawableStart="@drawable/ic_baseline_person"
                android:drawablePadding="6sp"
                android:hint="@string/user_name_text_view"
                android:importantForAutofill="no"
                tools:ignore="TextFields" />
    
            <EditText
                android:id="@+id/edtPassword"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:drawableStart="@drawable/ic_pass"
                android:drawablePadding="6sp"
                android:hint="@string/pass_word_text_view"
                android:importantForAutofill="no"
                tools:ignore="TextFields" />
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:gravity="center">
    
                <Button
                    android:id="@+id/btnLogin"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginEnd="40dp"
                    android:text="@string/login_button"
                    tools:ignore="ButtonStyle" />
    
                <Button
                    android:id="@+id/btnClear"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/clear_button"
                    tools:ignore="ButtonStyle" />
    
            </LinearLayout>
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="12dp"
                android:text="@string/information_text_view" />
    
    
        </LinearLayout>
    
        <ProgressBar
            android:id="@+id/prbLoading"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:visibility="visible" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    Code file .kt

    class LoginFragment : BaseFragment<FragmentLoginBinding>(), View.OnClickListener {
    
        private val loginViewModel: LoginViewModel by activityViewModels { LoginViewModel.Factory() }
    
        override fun createBinding(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): FragmentLoginBinding {
            return FragmentLoginBinding.inflate(inflater, container, false)
        }
    
        override fun observeLiveData() {
            loginViewModel.loginResult.observeNotNull(this) { resultMode ->
                when (resultMode.status) {
                    Status.INVALID -> resultMode.message?.let {
                        MessageDialog(it).show(
                            requireActivity().supportFragmentManager,
                            MessageDialog::class.java.name
                        )
                    }
    
                    Status.SUCCESS -> {
                        val action = LoginFragmentDirections.actionLoginFragmentToHomeFragment()
                        findNavController().navigate(action)
                    }
    
                    Status.ERROR -> resultMode.message?.let {
                        MessageDialog(it).show(
                            requireActivity().supportFragmentManager,
                            MessageDialog::class.java.name
                        )
                    }
    
                    else -> {
                        // do nothing
                    }
                }
            }
    
            loginViewModel.stateLoading.observeNotNull(this) { stateLoading ->
                binding.prbLoading.visibility = if (stateLoading.data == true) {
                    View.VISIBLE
                } else {
                    View.GONE
                }
            }
        }
    
        override fun initAction() {
            binding.btnLogin.setOnClickListener(this)
            binding.btnClear.setOnClickListener(this)
        }
    
        override fun onClick(v: View?) {
            when (v) {
                binding.btnLogin -> {
                    handleClickLogin()
                }
    
                binding.btnClear -> {
                    clearInputData()
                }
            }
        }
    
        private fun handleClickLogin() {
            val username = binding.edtUserName.text.toString()
            val password = binding.edtPassword.text.toString()
            val inputUser = User(username = username, password = password)
    
            loginViewModel.clickLogin(inputUser)
        }

    Kết Luận

    Cũng giống như MVP, MVVM thực hiện abtract trạng thái và thể hiện của View, cho phép chúng ta phân tách rõ ràng việc phát triển giao diện với xử lý business logic. MVVM đã kế thừa những ưu điểm vốn có của MVP, kết hợp với những lợi thế của data binding đem đến một pattern có khả năng phân chia các thành phần với từng chức năng riêng biệt, dễ dàng trong việc maintain, redesign. MVVM cũng đem lại khả năng test rất dễ dàng.

    Trên đây là 1 số chia sẻ về MVVM. Nếu có gì chưa chính xác hoặc còn thiếu thì rất mong nhận được góp ý từ mọi người. Cảm ơn các bạn đã theo dõi bài viết!

  • Map, FlatMap,… trong Kotlin

    Map, FlatMap,… trong Kotlin

    Giới thiệu

    Kotlin sinh ra extension function làm cho việc tuân thủ nguyên tắc open/close trở nên dễ dàng hơn. Với extension function ta không cần phải kế thừa từ một class khi muốn mở rộng chức năng.

    Có thể thấy extension function là thứ rất mạnh trong kotlin, cũng nhờ nó mà collections trong kotlin trở lên mạnh mẽ và làm cho việc lập trình đơn giản hơn rất nhiều.

    Bài viết này mình sẽ giới thiệu một số chức năng trong collections kotlin:

    1. filter
    2. map
    3. flatMap
    4. groupBy
    5. partition

    1. filter

    Chức năng này trả về một list mới gồm các phần tử thỏa mãn predicate.

    inline fun <T> Iterable<T>.filter(
        predicate: (T) -> Boolean
    ): List<T>
    

    Ví dụ trả về danh sách các số chẵn:

    listOf(1,2,3,4,5).filter { number->
        number%2 == 0
    }
    

    2. map

    Chức năng này giúp chuyển đổi một collection thành một list sau phép biến đổi hàm ánh xạ transform T -> R mà ta định nghĩa.

    inline fun <T, R> Iterable<T>.map(
        transform: (T) -> R
    ): List<R>
    

    Kiểu trả về của danh sách sau phép map phụ thuộc vào kiểu trả về của lambda transform.

    Ví dụ: Cho một danh sách sinh viên, điểm gpa cuối khóa sẽ được sử dụng để xét làm giảng viên. Một sinh viên được làm giảng viên nếu GPA > 3.8. Trả về danh sách giảng viên.

    Ta sẽ kết hợp filter và map để làm ví dụ xàm này.

    data class Student(val name:String, val gpa: Float)
    
    data class Teacher(val name:String)
    
    val students = getStudents()
    val teachers = students.filter { it.gpa > 3.8 }
            .map { Teacher(it.name) }
    

    Có thể thấy dùng filter + map làm code ngắn gọn dễ hiểu hơn nhiều đúng không nào.

    3. flatMap

    Nhìn thằng này chả khác gì thằng map ngoài thêm prefix flat, với một thằng ngu english như mình sau khi dịch flat thì chị google cho mình kết quả là phẳng, phẳng như nào thì cùng xem tiếp nhé.

    Đây là hàm flatMap

    inline fun <T, R> Iterable<T>.flatMap(
        transform: (T) -> Iterable<R>
    ): List<R>
    

    flatMap khác map duy nhất chỗ kiểu trả về của lambda transform là Iterable<R> thay vì R.

    flatMap sẽ map mỗi phần tử kiểu T thành một Iterable<R> . Cùng xem cách mà flatMap hoạt động dưới đây ta sẽ hiểu sao lại phẳng.

    Các list trả về sau khi mỗi phần tử transform sẽ được addAll vào một list duy nhất là destination. Nếu dùng map thì sẽ trả về một list mà các phần tử trong đó là các list có độ dài khác nhau.

    public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
        //Trả về giá trị của hàm flatMapTo
        //transform là phép biến đổi T -> Iterable<R> mà ta truyền vào
        return flatMapTo(ArrayList<R>(), transform)
    }
    
    public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
        //Duyệt tất cả phần tử trong danh sách ban đầu
        for (element in this) {
    	//Với mỗi phần tử biến đổi thành một list bởi hàm transform
            val list = transform(element)
    	//Thêm vào arrayList
            destination.addAll(list)
        }
        return destination
    }
    

    flatMap rất hữu ích khi muốn transform một list với quan hệ 1-N.

    Ví dụ: Một ứng dụng nghe nhạc có chức năng cho người dùng chọn nhiều thể loại và trả về tất cả các bài hát trong các thể loại đó. Người dùng lại tài khoản vip nên mỗi bài hát lấy ra sẽ được chuyển định dạng thành vip.

    Ta có thể thấy thể loại và bài hát có quan hệ 1-N ta nghĩ ngay đến flatMap.

    data class Song(val name: String, val singer: String)
    
    data class VipSong(val name: String, val singer: String)
    
    data class Category(val name: String, val songs: List<Song>)
    ...
    val categories = listOf<Category>()
    val vipSongs = categories.flatMap { category ->
        category.songs.map { VipSong(it.name, it.singer) }
    }
    

    4. groupBy

    Hàm này có chức năng nhóm các phần tử trong collection theo selector và trả về một map collection với các key theo selector. Ví dụ: Liệt kê các học sinh theo tên cuong -> {Student("cuong", id = 1)}

    data class Grade(val math: Float, val physics: Float)
    
    data class Student(val name: String, val grade: Grade)
    ...
    
    val students = listOf(
            Student("Cuong1", Grade(1f,2f)),
            Student("Cuong2", Grade(3f,3f)),
            Student("Cuong3", Grade(1f,2f)),
            Student("Cuong5", Grade(1.2f,2.5f))
        )
    print(students.groupBy {
        it.grade
    })
    

    Kết quả sẽ trả về một map collection:

    {
    	Grade(math=1.0, physics=2.0)=[Student(name=Cuong1, grade=Grade(math=1.0, physics=2.0)), Student(name=Cuong3, grade=Grade(math=1.0, physics=2.0))],
    	Grade(math=3.0, physics=3.0)=[Student(name=Cuong2, grade=Grade(math=3.0, physics=3.0))], 
    	Grade(math=1.2, physics=2.5)=[Student(name=Cuong5, grade=Grade(math=1.2, physics=2.5))]
    }
    

    5. Partition

    Hàm này có chức năng phân chia collection thành hai list theo điều kiện, một list là thỏa mãn điều kiện, một list là không thỏa mãn điều kiện.

    Ví dụ: Cần lấy danh sách sinh viên được tốt nghiệp và sinh viên chưa được tốt nghiệp

    data class Student(val name: String, val gpa: Float, val credits: Int)
    
    fun isGraduated(student: Student) = student.run { gpa>=2.5 && credits == 150}
    
    fun main() {
        val students = listOf(
            Student("Lan", 4.0f, 140),
            Student("Phuong", 2.45f, 150),
            Student("MA", 2.7f, 150)
        )
        val (graduated, undergraduate) = students.partition(::isGraduated)
        println("""
            graduated: $graduated
            undergraduate: $undergraduate
        """.trimIndent())
    }
    

    Kết quả:

    graduated: [Student(name=MA, gpa=2.7, credits=150)]
    undergraduate: [Student(name=Lan, gpa=4.0, credits=140), Student(name=Phuong, gpa=2.45, credits=150)]
    

    Kết luận

    Các chức năng này giúp chúng ta giảm thiếu số lượng mã cũng như code clear hơn, nó cũng quan trọng và gặp thường xuyên khi làm việc với các thư viện reactive như Rx, flow coroutine.

  • Tìm hiểu về Regular Expression

    Tìm hiểu về Regular Expression

    Tìm hiểu về Regular Expression

    Mở đầu

    RegEx là một khái niệm chúng ta thường xuyên gặp qua. Các master thì sẽ chẳng lạ lùng gì, còn với newbie thì cực kỳ khó hiểu và tiếp cận.

    Bài viết này mình sẽ giới thiệu qua về RegEx. Khi thành thạo được công cụ này, chắc chắn cuộc sống của lập trình viên sẽ "tươi đẹp" hơn rất nhiều.

    RegEx là gì ?

    RegEx (Regular Expression) trong tiếng Việt được gọi là Biểu thức chính quy. Regex không phải là một ngôn ngữ lập trình, nó chỉ là một bộ cú pháp dùng để bắt chuỗi. Regex là các ký tự được kết hợp với nhau theo quy tắc để tạo nên một trình tự giúp chúng ta tìm kiếm và thay thế văn bản một cách thông minh, nhanh chóng, đơn giản và thuận tiện. Nó có thể dùng được trong hầu hết các ngôn ngữ lập trình bậc cao như Java, C#, Python, JS, PHP,…

    Ứng dụng của Regular Expression

    • Ứng dụng trong kiểm tra tính hợp lệ
      • Kiểm tra email có hợp lệ hay không
      • Kiểm tra số điện thoại ở Việt Nam
      • Kiểm tra URL hợp lệ
      • Kiểm tra độ dài của câu có nằm trong khoảng (a,b) hay không
      • Bất cứ cái gì có quy tắc bạn đều có thể kiểm tra với Regex
    • Ứng dụng trong tìm kiếm và thay thế

    Cú pháp của biểu thức chính quy

    Chuỗi so khớp cơ bản

    Chuỗi Ý nghĩa
    . Khớp bất kỳ ký tự nào
    \d Khớp với số bất kỳ từ 0 – 9
    \D Phủ định của \d
    \w Khớp với các chữ cái tiếng anh, chữ số và dấu _
    \W Phủ định của \w
    <dấu cách> Khớp với dấu cách(SPACE trên bàn phím)
    \t Khớp với dấu tab
    \n Khớp với new line(xuống hàng)
    \s Khớp với dấu trắng bất kỳ(dấu cách, \t,\n)
    \S Phủ định của \s

    Kết hợp các chuỗi so khớp

    Bạn có thể kết hợp các chuỗi so khớp lại với nhau bằng cách đưa chúng vào trong cặp ngoặc vuông. Ví dụ :

    • [abc] -> Khớp các ký tự hoặc là a, hoặc là b, hoặc là c
    • [a-fA-Z] -> a-f là tất cả các ký tự từ a đến f trong bảng chữ cái tiếng anh đó, A-Z tương tự là từ A hoa đến Z hoa. Vậy là regex này khớp với mọi ký tự trong bảng chữ cái tiếng anh bất kể hoa thường.
    • [\d,] -> Khớp với ký tự số hoặc dấu ,

    Các ký tự ranh giới

    Ký tự Ý nghĩa Demo
    \b Xác định ranh giới của từ demo
    \B Phủ định của \b demo
    ^ Xác định vị trí bắt đầu dòng demo
    $ Xác định vị trí kết thúc dòng demo

    Sử|+ dụng hoặc trong regex

    (0|84|+84) : Hoặc 0, hoặc 84, hoặc +84 – demo

    Các ký tự định lượng

    Loại định lượng Ý nghĩa Demo
    X* So khớp lặp lại regex X 0 hoặc vô số lần demo
    X+ So khớp lặp lại regex X 1 hoặc vô số lần demo
    X? So khớp lặp lại regex X 0 hoặc 1 lần demo
    X{m} So khớp lặp lại regex X chính xác m lần demo
    X{m,} So khớp lặp lại regex X m hoặc nhiều hơn m lần demo
    X{m,n} So khớp lặp lại regex X với số lần từ m tới n(bao gồm cả m và n) demo

    Và một số ví dụ sau:

    • ab+ : khớp các chuỗi ab, abb, abbaabab,…
    • (ab)+ : khớp các chuỗi ab, abab, ababab,…

    Các ký tự đặc biệt

    Các ký tự {}[]()^$.|*+?\ và dấu - ở trong cặp ngoặc vuông là các ký tự đặc biệt. Nếu bạn muốn dùng nó để so khớp thì cần thêm dấu \ vào đằng trước:

    Ví dụ: \. sẽ khớp với dấu chấm .\\ sẽ khớp với ký tự \.

    Một số RegEx được dùng phổ biến

    So khớp email

    • ^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$
    • ^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$
    • ^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|((a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$
    • ^\w+[\w-\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$
    • ^.+@.+$

    So khớp URL

    • ^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$
    • ((mailto\:|(news|(ht|f)tp(s?))\://){1}\S+)
    • (http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?
    • ^(http|https|ftp)\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(:[a-zA-Z0-9]*)?/?([a-zA-Z0-9\-\._\?\,\’/\\\+&amp;%\$#\=~])*$

    So khớp SĐT ở Việt Nam

    • \+?(0|84)\d{9}

    So khớp số nguyên

    • \b\d+\b
    • \b(?:\d{3}\.)+\d{3}\bdemo

    So khớp mã HTML

    So khớp tên riêng

    • (?:[A-Z]\p{L}+ ){1,3}[A-Z]\p{L}+demo

    Một số liên kết hữu ích

    1. Test RegEx online: https://regex101.com/
    2. RegEx cheetsheet: http://web.mit.edu/hackl/www/lab/turkshop/slides/regex-cheatsheet.pdf
    3. Thư viện regex: http://regexlib.com/

    Kết bài

    Trên đây là một số kiến thức "nhập môn" về RegEx. Hi vọng bài viết nhỏ này đã giúp bạn học được cú pháp viết RegEx và áp dụng vào công việc thường ngày.