Sử dụng Protocol Buffers trong C++ và Caffe

2016/10/23

Protocol Buffers (ProtoBuf hay PB) là một trong những thành phần quan trọng trong thư viện Caffe. Bài viết dưới đây là một số note và cách sử dụng Protocol Buffers trong ngôn ngữ C++. Mục tiêu ban đầu chủ yếu tìm hiểu xem khả năng tuỳ chỉnh của Caffe đến đâu.

Giới thiệu

Protocol Buffers là một cơ chế lưu trữ và thao tác dành cho dữ liệu có cấu trúc do Google phát triển. Protocol Buffers có thể so sánh giống như XML, tuy nhiên PB có một số ưu điểm (theo quảng cáo của Google): tiện hơn, nhẹ hơn (từ 3 đến 10 lần so với XML), thao tác đọc ghi nhanh hơn (20 đến 100 lần nhanh hơn so với XML) và dễ dàng hơn. Protocol Buffers cho phép người dùng thiết kế dữ liệu và sử dụng code tự động phát sinh để đọc ghi các dữ liệu đó.

Cơ chế

Người dùng định nghĩa cấu trúc dữ liệu của mình trong file .proto. Trong đó định nghĩa kiểu dữ liệu, ràng buộc của các thuộc tính cũng như cấu trúc (mảng hay giá trị scalar). Trình biên dịch PB sẽ dịch file .proto thành một file mà ngôn ngữ lập trình đọc được (trong trường hợp C++ là file .h) giúp lập trình viên thao tác trên dữ liệu được thiết kế trước đó. Và như vậy, thay vì tự thiết kế 1 dữ liệu bằng chính ngôn ngữ lập trình, và tốn thêm thời gian để viết các cơ chế đọc ghi, chuyển đổi dữ liệu hay chuyển từ định dạng này (text) sang định dạng khác (binary), ProtoBuf sẽ tự động sinh code cho những thao tác này.

Trong Caffe

Protocol Buffers đóng vai trò khá quan trọng trong thư viện Caffe. Nếu không sử dụng PB thì Caffe trở thành một thư viện rất khó xài bởi nhiều lí do:

  1. Không có tài liệu cụ thể để tuỳ chỉnh các layer hay tự thiết kế mạng cho riêng hệ thống. Vấn đề này được giải quyết đơn giản nhờ PB. PB cho phép cá c tác giả của Caffe tạo ra các dữ liệu dùng cho hệ thống này (Solver, Layer, Net, …). Những ai dùng Caffe chỉ cần bỏ chút thời gian để đọc lại đoạn thiết kế trong file proto này đã được comment khá đầy đủ và chi tiết. Nhờ việc open source phần này nên các nhóm nghiên cứu khác có khả năng custom bằng cách thêm các Layer hoặc thêm các hàm vàotrong hệ thống Caffe.

  2. Khả năng không custom cao: việc tạo ra một kiến trúc mới khá dễ dàng nhờ vào PB. Việc thiết kế một mạng giờ đây giống như một trò xếp hình với các khối là những kiểu dữ liệu được thiết kế trong ProtoBuf [Hình 1]. Một ví dụ kinh điển là sử dụng Caffe để thiết kế mạng LeNet. Người nghiên cứu không phải quá nặng đầu hay nhũn não để viết code C++ hay Matlab hay Python hay Cuda để thiết kế mạng mà thay vào đó là viết một kiến trúc trong file prototxt như thiết kế một cấu trúc XML hoặc JSON. Tác giả Caffe cũng không nhũn não khi ngồi parse từ file text sang kiểu dữ liệu của mình.

Mạng Deep Learning

Cài đặt

Clone mã nguồn của ProtoBuf tại Github, sau đó cài theo lệnh

    ./autogen.sh
    ./configure
    make
    make install

Thiết kế dữ liệu

Dữ liệu được thiết kế trong một file proto. Dưới đây là ví dụ trong website của Google, mục đích để quản lý danh bạ điện thoại.

    package tutorial;

    message Person {
      required string name = 1;
      required int32 id = 2;
      optional string email = 3;

      enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
      }

      message PhoneNumber {
        required string number = 1;
        optional PhoneType type = 2 [default = HOME];
      }

      repeated PhoneNumber phone = 4;
    }

    message AddressBook {
      repeated Person person = 1;
    }

Để biên dịch file này, ta gõ lệnh

    protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

Trong đó $SRC_DIR là thư mục chứa mã nguồn, $DST_DIR là thư mục chứa các code cpp mà ProtoBuf sẽ phát sinh, addressbook.proto là file chứa dữ liệu danh bạ. Trước khi xem thử trong ProtoBuf viết cái chi chi, ta xem thử ProtoBuf đã phát sinh ra 2 file addressbook.pb.ccaddressbook.pb.h. Trong đó chứa tất cả phần định nghĩa và cài đặt của các Lớp đối tượng Person, PhoneNumber. Chính xác là ProtoBuf đã làm giúp dev chuyện thiết kế lớp, viết phần truy xuất và chuyển đổi kiểu dữ liệu, thao tác các thuật tính (kiểm tra thuộc tính, getter và setter). Xong, ta quay lại phần thiết kế của ProtoBuf.

Cú pháp

Trong bài viết mình chỉ giới thiệu rất vắn tắt về các từ khoá sử dụng trong ProtoBuf nhằm có thể tìm hiểu cơ chế thiết kế dữ liệu trong Caffe. Chi tiết hơn về cứ pháp ngôn ngữ này có thể tham khảo ở trang tài liệu của ProtoBuf.

  1. package: tương đương với namespace trong C++.
  2. message: tương đương với Class trong C++.
  3. int32 ,string , enum: tương đương với các kiểu dữ liệu trong C++. Ngoài ra trong ProtoBuf còn có bool, float, double.
  4. Các giá trị 1, 2, … đằng sau các khai báo: các tag phân biệt nhau trong một message.
  5. Có 3 modifier như sau: required, optional, repeated:

    a. required: trường bắt buộc phải tồn tại trong 1 lớp. b. optional: trường có thể tồn tại hoặc không trong một lớp. c. repeated: trường là một mảng động (không có phần tử nào hoặc có 1 hay nhiều phần tử).

Trong Caffe

Bây giờ quay trở lại file proto của Caffe và dường như mọi thứ đã quá rõ ràng. Với mỗi hàm activation, hàm loss, các Layer (convolution, reshape, filter, …) và Blob (chứa dữ liệu) Caffe thiết kế các lớp chính là nơi chứa các tham số cho các hàm, layer và data này.

Một điều khá thú vị đó là thư viện TensorFlow của Google cũng sử dụng ProtoBuf cho project của họ.

Nếu như chú ý kĩ, trên website của Caffe không liệt kê toàn bộ các tham số có thể cấu hình cho các Layer trong thiết kế mạng. Có thể kể đến ở đây như Data Layer, trong tham số transform_data có scale, mean_file_size, mirrorcrop_size; nhưng nếu xem trong phần định nghĩa của protobuf, ta thấy ngoài ra còn có mean_file, mean_value, force_colorforce_gray, qua đó ta thấy được rõ ràng xem protobuf có lợi hơn là xem trong phần documents. Một điểm thuận lợi khác là file caffe.proto được cập nhật liên tục so với document trên website.

Lấy một ví dụ minh hoạ trong mạng AlexNet. Có 2 file prototxt rất đáng quan tâm và hầu như ở các model được cung cấp sẵn đều có đó là: train_val.prototxtsolver.txt.

train_val.prototxt

Đây là file chứa kiến trúc mạng của mô hình được cung cấp. Trong này định nghĩa toàn bộ kiến trúc (các tầng, tham số của mỗi tầng) cũng như định nghĩa dữ liệu đầu vào và output đầu ra cho mạng deep.

    name: "AlexNet"
    layer {
      name: "data"
      type: "Data"
      top: "data"
      top: "label"
      include {
        phase: TRAIN
      }
      transform_param {
        mirror: true
        crop_size: 227
        mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
      }
      data_param {
        source: "examples/imagenet/ilsvrc12_train_lmdb"
        batch_size: 256
        backend: LMDB
      }
    }
    layer {
      name: "data"
      type: "Data"
      top: "data"
      top: "label"
      include {
        phase: TEST
      }
      transform_param {
        mirror: false
        crop_size: 227
        mean_file: "data/ilsvrc12/imagenet_mean.binaryproto"
      }
      data_param {
        source: "examples/imagenet/ilsvrc12_val_lmdb"
        batch_size: 50
        backend: LMDB
      }
    }

Thành phần đầu tiên là name, tên của mạng. Các phần tử tiếp theo là các layer, tất cả các tham số liên quan đến layer đều nằm trong khối ngoặc <span class="p">{</span> <span class="w"></span> <span class="p">}</span>. Có 3 thành phần chắc chắn có của một layer:

  1. name: tên của layer. Caffe dựa vào tên layer để xác định xem layer đó ở đâu trong mạng.
  2. type: chắc chắn rồi, Caffe cần biết đây là layer Input hay là layer Convolution hay là ReLu hay là Softmax.
  3. top/bottom: xác định xem layer hiện tại sẽ nằm trên layer nào và sẽ nằm dưới layer nào. Vì kiến trúc mạng kiểu xếp tầng thế này rất thuận tiện để thiết kế mạng CNN. Và điểm bất lợi đó là ta khó có thể thiết kế các mạng có cấu trúc phức tạp hơn, kiểu như đồ thì chẳng hạn.

Ngoài 3 thuộc tính kể trên, mỗi layer sẽ có những tham số khác, phụ thuộc vào loại layer và tính chât của layer đó trong mạng.

solver.prototxt

Đây là tập tin chứa tham số trong quá trình train mạng deep và các tham số trong quá trình forward cũng như lan truyền ngược.

    net: "models/bvlc_alexnet/train_val.prototxt"
    test_iter: 1000
    test_interval: 1000
    base_lr: 0.01
    lr_policy: "step"
    gamma: 0.1
    stepsize: 100000
    display: 20
    max_iter: 450000
    momentum: 0.9
    weight_decay: 0.0005
    snapshot: 10000
    snapshot_prefix: "models/bvlc_alexnet/caffe_alexnet_train"
    solver_mode: GPU

Ta dễ dàng thấy ở đây có net: dẫn đến file chứa kiến trúc mạng. Một số tham số khá quen thuộc như: momentum, max_iter, có cả một thuộc tính nhằm set xem mạng sẽ chạy ở GPU hay CPU (solver_mode). Cũng giống như các tham số ở train_val.prototxt, tất cả các thông tin về cấu hình được đề cập trong file caffe.proto.

Tổng kết

Protobuf đóng vai trò quan trọng trong công cụ Caffe, giúp các nhà nghiên cứu có thể dễ dàng xây dựng, chỉnh sửa, cũng như chia sẻ kiến trúc mạng của mình với cộng đồng. Ngoài ra protobuf còn giúp nhà lập trình dễ dàng trong quá trình xây dựng hệ thống phần mềm cũng như mã nguồn. Đối với người dùng, việc xem xét, hiệu chỉnh trên file prototxt thuận lợi và tốn ít công sức hơn so với đọc từ code C++ hay kể cả matlab, cũng như có một “ngôn ngữ chung” cho thiết kế mạng deep.