Chương trước đã trình bày cách văn bản phi cấu trúc (ví dụ: báo cáo kết quả kinh doanh, bài báo tin tức, phần mô tả hoạt động kinh doanh) có thể được chuyển thành các tín hiệu có cấu trúc cho phân tích tài chính. Chương này mở rộng bộ công cụ dữ liệu thay thế sang một dạng thức thứ hai: hình ảnh. Dữ liệu thị giác rất dồi dào trong bối cảnh tài chính nhưng bị khai thác chưa đầy đủ một cách có hệ thống. Ảnh vệ tinh phản ánh hoạt động kinh tế thực (ví dụ: mức độ lấp đầy bãi đỗ xe tại các điểm bán lẻ, tiến độ xây dựng tại các khu công nghiệp, độ sáng ban đêm như một biến đại diện cho GDP khu vực, lưu lượng tàu tại các cảng, sức khỏe cây trồng trên các vùng nông nghiệp). Các tài liệu của doanh nghiệp đến dưới dạng PDF quét, với các bảng biểu và hình vẽ khó trích xuất bằng các phương pháp trích xuất văn bản tiêu chuẩn. Các biểu đồ tài chính mã hóa thông tin mà các nhà phân tích diễn giải bằng mắt nhưng các chiến lược có hệ thống không thể sử dụng nếu chưa được số hóa. Và nội dung thị giác của mạng xã hội, quảng cáo và hình ảnh sản phẩm mang các tín hiệu về cảm xúc và thương hiệu bổ sung cho phân tích văn bản.
Thách thức cốt lõi nằm ở khâu biểu diễn: một hình ảnh là một tensor ba chiều gồm các cường độ điểm ảnh, không có cấu trúc ngữ nghĩa vốn có. Việc chuyển mảng thô này thành một tín hiệu tài chính (tức là một con số dự báo lợi nhuận, đo lường rủi ro hoặc đại diện cho hoạt động kinh tế) đòi hỏi hoặc kỹ thuật thiết kế đặc trưng thủ công, hoặc các biểu diễn được học thông qua mạng nơ-ron tích chập sâu (CNN) và vision transformer (ViT). Chương này trình bày cả hai cách tiếp cận.
Chúng ta tổ chức nội dung xoay quanh năm lĩnh vực ứng dụng, mỗi lĩnh vực có nguồn dữ liệu, yêu cầu mô hình hóa và động cơ kinh tế riêng. Thứ nhất, ảnh vệ tinh và ảnh địa không gian để dự báo tức thời (nowcasting) hoạt động kinh tế. Thứ hai, phân tích ảnh tài liệu để trích xuất dữ liệu có cấu trúc từ các hồ sơ tài chính tiếng Việt. Thứ ba, số hóa biểu đồ và hình vẽ cho kiểm định ngược có hệ thống. Thứ tư, phân tích cảm xúc thị giác từ mạng xã hội và truyền thông tin tức. Thứ năm, hợp nhất đa phương thức, kết hợp tín hiệu hình ảnh và văn bản thành các mô hình dự báo chung.
Thị trường Việt Nam có những cơ hội đặc thù trong lĩnh vực này. Ảnh vệ tinh đặc biệt hữu ích trong một nền kinh tế có các ngành nông nghiệp và sản xuất lớn, nơi dữ liệu thực địa đến với độ trễ đáng kể. Các hồ sơ tài chính của Việt Nam thường được phát hành dưới dạng ảnh quét thay vì định dạng máy đọc được, khiến AI xử lý tài liệu trở thành điều thiết yếu chứ không phải tùy chọn. Và quá trình đô thị hóa nhanh chóng thể hiện trong ảnh xây dựng và hạ tầng cung cấp các biến đại diện tần suất cao cho động lực kinh tế vĩ mô mà thống kê chính thức không thể sánh được.
Hiện mã
import pandas as pdimport numpy as npfrom pathlib import Pathimport warningswarnings.filterwarnings("ignore")# Core image processingfrom PIL import Imageimport io# Deep learningimport torchimport torch.nn as nnimport torchvision.transforms as transformsimport torchvision.models as models# Visualizationimport plotnine as p9from mizani.formatters import percent_format, comma_formatimport matplotlib.pyplot as plt# Statistical analysisfrom scipy import statsimport statsmodels.api as smimport statsmodels.formula.api as smffrom linearmodels.panel import PanelOLS
46.1 Nền tảng: Từ điểm ảnh đến tín hiệu tài chính
46.1.1 Biểu diễn hình ảnh
Một hình ảnh số là một hàm \(I: \{1, \ldots, H\} \times \{1, \ldots, W\} \times \{1, \ldots, C\} \rightarrow [0, 255]\) ánh xạ tọa độ không gian và các kênh màu thành các giá trị cường độ. Với một ảnh RGB có chiều cao \(H\) và chiều rộng \(W\), biểu diễn là một tensor \(\mathbf{I} \in \mathbb{R}^{H \times W \times 3}\). Một ảnh RGB \(224 \times 224\) đơn lẻ (tức là đầu vào tiêu chuẩn của các CNN hiện đại) chứa \(224 \times 224 \times 3 = 150{,}528\) chiều. Số chiều cực lớn này, kết hợp với cấu trúc không gian (các điểm ảnh lân cận tương quan với nhau), khiến hình ảnh khác biệt căn bản so với dữ liệu tài chính dạng bảng và đòi hỏi các kiến trúc chuyên biệt.
Điểm mấu chốt của mạng nơ-ron tích chập là chia sẻ tham số thông qua các bộ lọc cục bộ. Một lớp tích chập áp dụng một nhân \(\mathbf{K} \in \mathbb{R}^{k \times k \times C_{\text{in}}}\) để tạo ra một bản đồ đặc trưng:
\[
(\mathbf{I} * \mathbf{K})(i, j) = \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} \sum_{c=1}^{C_{\text{in}}} I(i+m, j+n, c) \cdot K(m, n, c)
\tag{46.1}\]
Bằng cách xếp chồng các lớp tích chập với các hàm phi tuyến và các phép gộp (pooling), mạng xây dựng một hệ thống phân cấp các biểu diễn: các lớp đầu phát hiện cạnh và kết cấu; các lớp giữa phát hiện các bộ phận và mẫu hình; các lớp sâu phát hiện đối tượng và cảnh vật. Đầu ra của lớp cuối \(\mathbf{z} \in \mathbb{R}^{d}\) (với \(d\) thường là 512-2048) là một biểu diễn gọn của nội dung ngữ nghĩa của hình ảnh, có thể được dùng trực tiếp làm vector đặc trưng cho dự báo tài chính.
Hình ảnh tài chính trải rộng trên nhiều độ phân giải và dạng thức:
Bảng 46.1: Các nguồn dữ liệu hình ảnh cho thị trường tài chính Việt Nam
Nguồn
Độ phân giải
Kênh
Kích thước điển hình
Tần suất cập nhật
Vệ tinh Sentinel-2
10m/pixel
13 băng tần
10,980 × 10,980
5 ngày
Planet Labs
3m/pixel
4 băng tần
4,000 × 4,000
Hằng ngày
Độ sáng ban đêm VIIRS
500m/pixel
1 (DNB)
3,000 × 1,800
Tổng hợp hằng tháng
Bản quét báo cáo thường niên
300 DPI
3 (RGB)
2,480 × 3,508
Hằng năm
Ảnh chân dung CEO
Thay đổi
3 (RGB)
500 × 500
Hằng năm
Ảnh tin tức
Thay đổi
3 (RGB)
800 × 600
Thời gian thực
Biểu đồ tài chính
Thay đổi
3 (RGB)
1,000 × 600
Thời gian thực
46.1.2 Học chuyển giao cho tài chính
Huấn luyện một CNN từ đầu đòi hỏi hàng triệu ảnh có gán nhãn, nhiều hơn rất nhiều so với bất kỳ ứng dụng tài chính nào có thể cung cấp. Học chuyển giao (transfer learning) giải quyết vấn đề này bằng cách dùng các mạng đã được tiền huấn luyện trên ImageNet (1,2 triệu ảnh, 1.000 lớp) làm bộ trích xuất đặc trưng. Mạng tiền huấn luyện đã học được các biểu diễn thị giác chung (cạnh, kết cấu, hình dạng, đối tượng); chúng ta chỉ cần thay lớp phân loại cuối cùng bằng một lớp đầu ra (head) riêng cho tác vụ.
Về mặt hình thức, gọi \(f_{\boldsymbol{\theta}}(\mathbf{I})\) là một mạng tiền huấn luyện với các tham số \(\boldsymbol{\theta}\) được chia thành bộ trích xuất đặc trưng \(\boldsymbol{\theta}_{\text{feat}}\) và bộ phân loại \(\boldsymbol{\theta}_{\text{cls}}\). Đối với các ứng dụng tài chính, chúng ta:
Trích xuất đặc trưng: Đóng băng \(\boldsymbol{\theta}_{\text{feat}}\), trích xuất \(\mathbf{z} = f_{\boldsymbol{\theta}_{\text{feat}}}(\mathbf{I})\), và huấn luyện một mô hình đơn giản (hồi quy tuyến tính, gradient boosting) trên \(\mathbf{z}\).
Tinh chỉnh: Khởi tạo từ \(\boldsymbol{\theta}\) và huấn luyện tất cả các tham số trên tác vụ tài chính với tốc độ học nhỏ để tránh quên thảm khốc (catastrophic forgetting).
Các họ kiến trúc chính là:
ResNet [He và c.s. (2016)]. Các kết nối phần dư (\(y = F(x) + x\)) cho phép huấn luyện các mạng rất sâu (50-152 lớp). Kết nối tắt giải quyết vấn đề triệt tiêu gradient. ResNet-50 tạo ra một vector đặc trưng 2.048 chiều từ lớp áp chót.
EfficientNet [Tan và Le (2019)]. Mở rộng kết hợp (compound scaling) đồng thời độ sâu, độ rộng và độ phân giải. EfficientNet-B0 đạt độ chính xác của ResNet-50 với 5,3 triệu tham số (so với 25,6 triệu), khiến nó khả thi để xử lý hàng nghìn ô ảnh vệ tinh.
Vision Transformer (ViT) [Dosovitskiy và c.s. (2020)]. Xem một hình ảnh như một chuỗi các mảnh ảnh (patch) \(16 \times 16\), xử lý chúng qua một bộ mã hóa Transformer tiêu chuẩn. ViT-B/16 tạo ra một vector nhúng 768 chiều. Đặc biệt hiệu quả đối với ảnh tài liệu, nơi các quan hệ không gian giữa các thành phần (bảng, tiêu đề, khối văn bản) đóng vai trò quan trọng.
Hiện mã
# DataCore.vn APIfrom datacore import DataCoredc = DataCore()def build_feature_extractor(model_name="resnet50", device="cpu"):""" Build a pre-trained CNN feature extractor. Parameters ---------- model_name : str One of 'resnet50', 'efficientnet_b0', 'vit_b_16'. device : str 'cpu' or 'cuda'. Returns ------- model : nn.Module Feature extraction model. transform : transforms.Compose Image preprocessing pipeline. """if model_name =="resnet50": weights = models.ResNet50_Weights.IMAGENET1K_V2 model = models.resnet50(weights=weights) model.fc = nn.Identity() # Remove classification head dim =2048elif model_name =="efficientnet_b0": weights = models.EfficientNet_B0_Weights.IMAGENET1K_V1 model = models.efficientnet_b0(weights=weights) model.classifier = nn.Identity() dim =1280elif model_name =="vit_b_16": weights = models.ViT_B_16_Weights.IMAGENET1K_V1 model = models.vit_b_16(weights=weights) model.heads = nn.Identity() dim =768 model = model.to(device).eval() transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])return model, transform, dimdef extract_features(image_paths, model, transform, device="cpu", batch_size=32):""" Extract deep features from a list of images. Parameters ---------- image_paths : list Paths to image files. model : nn.Module Feature extraction model. transform : transforms.Compose Preprocessing pipeline. Returns ------- np.ndarray : Feature matrix (n_images x feature_dim). """ features = []for i inrange(0, len(image_paths), batch_size): batch_paths = image_paths[i:i + batch_size] batch_tensors = []for path in batch_paths:try: img = Image.open(path).convert("RGB") tensor = transform(img) batch_tensors.append(tensor)exceptException: batch_tensors.append(torch.zeros(3, 224, 224)) batch = torch.stack(batch_tensors).to(device)with torch.no_grad(): batch_features = model(batch).cpu().numpy() features.append(batch_features)return np.vstack(features)
46.2 Ảnh vệ tinh và ảnh địa không gian
46.2.1 Hoạt động kinh tế nhìn từ không gian
Ảnh vệ tinh cung cấp các phép đo hoạt động kinh tế có tần suất cao và chi tiết về không gian, độc lập với và thường đi trước thống kê chính thức. Công trình nền tảng của Henderson, Storeygard, và Weil (2012) đã chứng minh rằng độ sáng ban đêm, được đo bởi Chương trình Vệ tinh Khí tượng Quốc phòng (DMSP), là một biến đại diện đáng tin cậy cho GDP, đặc biệt ở những quốc gia có thống kê chính thức nhiều nhiễu hoặc chậm trễ. Donaldson và Storeygard (2016) khảo cứu các ứng dụng rộng rãi của dữ liệu vệ tinh trong kinh tế học, từ năng suất nông nghiệp đến tăng trưởng đô thị và đo lường xung đột. Donaldson (2018) dùng các thước đo sản lượng nông nghiệp lấy từ vệ tinh để nghiên cứu lợi ích phúc lợi của đường sắt ở Ấn Độ thuộc địa. Jean và c.s. (2016) kết hợp ảnh vệ tinh ban ngày với các CNN để dự đoán nghèo đói từ không gian với \(r^2 > 0.7\).
Tài liệu kinh tế chính trị khai thác dữ liệu vệ tinh để trả lời những câu hỏi không thể giải đáp bằng thống kê chính thức. Hodler và Raschky (2014) dùng độ sáng ban đêm để cho thấy các vùng dưới cấp quốc gia có liên kết chính trị với nhà lãnh đạo quốc gia có mức hoạt động kinh tế cao hơn hẳn so với các vùng khác (ví dụ: một phát hiện về sự thiên vị theo vùng có liên quan trực tiếp đến việc phân bổ đầu tư cấp tỉnh của Việt Nam). Henderson và c.s. (2018) mở rộng khung độ sáng để nghiên cứu phân bố toàn cầu của hoạt động kinh tế, cho thấy địa lý, lịch sử và thương mại cùng quyết định hình thái phân bố không gian của sự thịnh vượng.
Đối với các ứng dụng tài chính, điểm mấu chốt là dữ liệu vệ tinh đến nhanh hơn lợi nhuận kế toán của doanh nghiệp hay thống kê của chính phủ. Doanh thu hằng quý của một nhà bán lẻ được báo cáo 4-8 tuần sau khi quý kết thúc; ảnh vệ tinh về các bãi đỗ xe của họ có sẵn trong vòng vài ngày. Lợi thế về thời gian này tạo ra một trường hợp sử dụng tự nhiên cho dự báo tức thời (tức là ước tính các điều kiện kinh tế hiện tại trước khi dữ liệu chính thức được công bố) và cho việc xây dựng các tín hiệu giao dịch dựa trên thông tin công khai nhưng tốn kém để xử lý.
46.2.2 Ứng dụng 1: Độ sáng ban đêm và GDP cấp tỉnh
Tổng cục Thống kê (GSO) của Việt Nam công bố GDP cấp tỉnh với độ trễ vài tháng. Độ sáng ban đêm từ cảm biến VIIRS (Visible Infrared Imaging Radiometer Suite) cung cấp một giải pháp thay thế gần như theo thời gian thực. Chúng ta xây dựng một thước đo mức độ phơi nhiễm ở cấp doanh nghiệp bằng cách liên kết địa điểm đăng ký của mỗi doanh nghiệp niêm yết với độ sáng của tỉnh đó.
Hiện mã
# Load nighttime luminosity data (VIIRS monthly composites)# Source: Earth Observation Group (EOG) / NOAAnightlights = dc.get_nightlight_data( start_date="2014-01-01", end_date="2024-12-31", resolution="province")# Load firm location datafirm_locations = dc.get_firm_locations()# Provincial GDP from GSOprovincial_gdp = dc.get_provincial_gdp( start_date="2014-01-01", end_date="2024-12-31")print(f"Nightlight observations: {len(nightlights)}")print(f"Provinces covered: {nightlights['province'].nunique()}")print(f"Firms with location: {firm_locations['ticker'].nunique()}")
Hiện mã
# Validate: does nightlight predict provincial GDP?nl_gdp = nightlights.merge( provincial_gdp, on=["province", "year", "quarter"], how="inner")# Log-log specification (standard in the literature)nl_gdp["ln_luminosity"] = np.log(nl_gdp["mean_radiance"].clip(lower=0.01))nl_gdp["ln_gdp"] = np.log(nl_gdp["provincial_gdp"].clip(lower=1))# Cross-sectional regression by yearvalidation_results = []for year in nl_gdp["year"].unique(): subset = nl_gdp[nl_gdp["year"] == year]iflen(subset) <20:continue model = sm.OLS( subset["ln_gdp"], sm.add_constant(subset["ln_luminosity"]) ).fit() validation_results.append({"year": year,"beta": model.params.iloc[1],"r_squared": model.rsquared,"n_provinces": int(model.nobs) })validation_df = pd.DataFrame(validation_results)print(f"Avg R² (ln GDP ~ ln Luminosity): {validation_df['r_squared'].mean():.3f}")
46.2.3 Ứng dụng 2: Ảnh vệ tinh cho dự báo tức thời theo ngành
Ngoài độ sáng ban đêm, ảnh vệ tinh ban ngày cung cấp các tín hiệu đặc thù theo ngành. Chúng ta triển khai ba kênh liên quan đến nền kinh tế Việt Nam.
Port activity. Vietnam is a major export-oriented economy. Satellite imagery of container ports (Cát Lái, Hải Phòng) captures trade throughput before customs statistics are released. Ship detection algorithms applied to synthetic aperture radar (SAR) imagery count vessels and estimate cargo volumes.
Tiến độ xây dựng. Bất động sản và xây dựng chiếm một tỷ trọng đáng kể trong GDP và vốn hóa thị trường của Việt Nam. Các thuật toán phát hiện thay đổi áp dụng cho ảnh quang học độ phân giải cao nhận diện các công trình khởi công, tỷ lệ hoàn thành và việc chuyển đổi mục đích sử dụng đất.
Giám sát nông nghiệp. Việt Nam là nước xuất khẩu hàng đầu về gạo, cà phê, cao su và thủy sản. Chỉ số khác biệt thực vật chuẩn hóa (NDVI), được tính từ dữ liệu vệ tinh đa phổ, cung cấp đánh giá về sức khỏe cây trồng:
trong đó \(\rho_{\text{NIR}}\) và \(\rho_{\text{Red}}\) là độ phản xạ ở các băng cận hồng ngoại và băng đỏ. NDVI nhận giá trị từ \(-1\) đến \(+1\), với các giá trị trên 0,3 cho thấy thực vật khỏe mạnh. Các độ lệch so với chuẩn theo mùa là biến đại diện cho các bất ngờ về năng suất cây trồng.
46.2.4 Trích xuất đặc trưng từ ảnh vệ tinh bằng CNN
Đối với ảnh vệ tinh thô (thay vì các chỉ số tính sẵn như NDVI), chúng ta dùng học chuyển giao từ các CNN để trích xuất các đặc trưng không gian. Cách tiếp cận theo Jean và c.s. (2016): dùng một CNN đã tiền huấn luyện trên ImageNet để trích xuất các vector đặc trưng từ các ô ảnh vệ tinh, sau đó hồi quy các kết quả kinh tế theo các đặc trưng này.
Hiện mã
def satellite_feature_pipeline(image_dir, model_name="resnet50"):""" Extract CNN features from satellite image tiles. Parameters ---------- image_dir : str or Path Directory containing satellite tiles (PNG/TIFF). model_name : str Pre-trained model to use. Returns ------- DataFrame : image_id, feature vector columns. """ image_dir = Path(image_dir) image_paths =sorted(image_dir.glob("*.png")) +sorted( image_dir.glob("*.tif") )ifnot image_paths:print("No images found.")return pd.DataFrame() device ="cuda"if torch.cuda.is_available() else"cpu" model, transform, dim = build_feature_extractor(model_name, device) features = extract_features(image_paths, model, transform, device)# Create DataFrame feature_cols = [f"feat_{i}"for i inrange(dim)] df = pd.DataFrame(features, columns=feature_cols) df["image_id"] = [p.stem for p in image_paths]return dfdef predict_economic_activity(features_df, labels_df, label_col, n_components=50):""" Predict economic activity from satellite image features. Uses PCA for dimensionality reduction, then ridge regression. Parameters ---------- features_df : DataFrame CNN features with image_id. labels_df : DataFrame Economic outcomes with image_id. label_col : str Target variable column name. n_components : int PCA components to retain. Returns ------- dict : R², coefficients, cross-validated performance. """from sklearn.decomposition import PCAfrom sklearn.linear_model import RidgeCVfrom sklearn.model_selection import cross_val_score merged = features_df.merge(labels_df, on="image_id") feature_cols = [c for c in features_df.columns if c.startswith("feat_")] X = merged[feature_cols].values y = merged[label_col].values# PCA pca = PCA(n_components=n_components) X_pca = pca.fit_transform(X) var_explained = pca.explained_variance_ratio_.sum()# Ridge regression with cross-validation ridge = RidgeCV(alphas=np.logspace(-3, 3, 20), cv=5) cv_scores = cross_val_score(ridge, X_pca, y, cv=5, scoring="r2") ridge.fit(X_pca, y)return {"r2_cv_mean": cv_scores.mean(),"r2_cv_std": cv_scores.std(),"r2_train": ridge.score(X_pca, y),"pca_var_explained": var_explained,"optimal_alpha": ridge.alpha_,"n_images": len(merged) }
46.3 Phân tích ảnh tài liệu
46.3.1 Vấn đề hồ sơ tài chính của Việt Nam
Một tỷ lệ đáng kể các bản công bố thông tin của doanh nghiệp Việt Nam (ví dụ: báo cáo thường niên, báo cáo tài chính, nghị quyết hội đồng quản trị, biên bản họp cổ đông) được phát hành dưới dạng ảnh PDF quét thay vì văn bản máy đọc được. Điều này tạo ra một nút thắt trong trích xuất dữ liệu: thông tin tồn tại nhưng bị mắc kẹt trong định dạng điểm ảnh. Không như các hồ sơ ở những thị trường phát triển hơn (nơi các quy định bắt buộc XBRL bảo đảm khả năng máy đọc được), các hồ sơ của Việt Nam đòi hỏi nhận dạng ký tự quang học (OCR) và phân tích bố cục trước khi có thể bắt đầu bất kỳ phân tích định lượng nào.
Quy trình AI xử lý tài liệu cho các hồ sơ tài chính Việt Nam gồm bốn giai đoạn:
Phân loại trang: Xác định những trang nào chứa báo cáo tài chính, thảo luận của ban điều hành, ý kiến kiểm toán, v.v.
Phân tích bố cục: Phát hiện cấu trúc không gian như tiêu đề, đoạn văn, bảng, hình vẽ, chú thích.
OCR: Chuyển các vùng ảnh thành văn bản, dùng các mô hình được tối ưu cho tiếng Việt.
Trích xuất có cấu trúc: Phân tích cú pháp văn bản đã nhận dạng thành dữ liệu có cấu trúc (ví dụ: số liệu doanh thu, các khoản mục bảng cân đối kế toán).
46.3.2 OCR cho tài liệu tài chính tiếng Việt
Standard OCR engines (Tesseract, Google Cloud Vision) struggle with Vietnamese financial documents due to the combination of Vietnamese diacritics (ă, ơ, ư, ê, etc.), mixed Vietnamese-English content, and complex table layouts. We implement a pipeline using PaddleOCR (which has strong CJK and Southeast Asian language support) and VietOCR (a Vietnamese-specific model based on the transformer architecture of Baek và c.s. (2019)).
Hiện mã
def ocr_financial_document(pdf_path, language="vi", engine="paddleocr"):""" OCR a Vietnamese financial document (scanned PDF). Parameters ---------- pdf_path : str Path to PDF file. language : str Language code. engine : str 'paddleocr' or 'vietocr'. Returns ------- list[dict] : Per-page OCR results with bounding boxes. """from pdf2image import convert_from_path# Convert PDF pages to images pages = convert_from_path(pdf_path, dpi=300) results = []if engine =="paddleocr":from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="vi", use_gpu=False)for page_num, page_img inenumerate(pages):# Convert PIL to numpy img_array = np.array(page_img) ocr_result = ocr.ocr(img_array, cls=True) page_texts = []for line in ocr_result[0]: bbox, (text, confidence) = line page_texts.append({"text": text,"confidence": confidence,"bbox": bbox,"page": page_num +1 }) results.extend(page_texts)return resultsdef classify_page_type(ocr_results, page_num):""" Classify a document page by content type using keyword matching. Returns one of: 'balance_sheet', 'income_statement', 'cash_flow', 'notes', 'audit', 'management', 'other'. """ page_text =" ".join( [r["text"] for r in ocr_results if r["page"] == page_num] ).lower()# Vietnamese financial statement keywords keyword_map = {"balance_sheet": ["bảng cân đối kế toán", "tài sản", "nguồn vốn","nợ phải trả", "vốn chủ sở hữu" ],"income_statement": ["kết quả hoạt động kinh doanh", "doanh thu","lợi nhuận", "chi phí", "thu nhập" ],"cash_flow": ["lưu chuyển tiền tệ", "dòng tiền","hoạt động kinh doanh", "hoạt động đầu tư" ],"audit": ["báo cáo kiểm toán", "kiểm toán viên","ý kiến kiểm toán", "trung thực và hợp lý" ],"management": ["ban giám đốc", "hội đồng quản trị","báo cáo thường niên", "tình hình hoạt động" ] } scores = {}for page_type, keywords in keyword_map.items(): scores[page_type] =sum(1for kw in keywords if kw in page_text )ifmax(scores.values()) ==0:return"other"returnmax(scores, key=scores.get)
46.3.3 Trích xuất bảng từ báo cáo tài chính
Tác vụ trích xuất có giá trị cao nhất là khôi phục các bảng có cấu trúc từ báo cáo tài chính. Chúng ta triển khai một cách tiếp cận hai giai đoạn: trước tiên phát hiện các vùng bảng bằng một mô hình phân tích bố cục, sau đó phân tích cú pháp các vùng được phát hiện thành cấu trúc hàng-cột.
Hiện mã
def extract_tables_from_page(page_image, ocr_results, page_num):""" Extract structured tables from a document page. Uses spatial clustering of OCR bounding boxes to identify table regions, then aligns text into rows and columns. Parameters ---------- page_image : PIL.Image Page image. ocr_results : list[dict] OCR results for this page. page_num : int Page number. Returns ------- list[pd.DataFrame] : Extracted tables as DataFrames. """ page_texts = [r for r in ocr_results if r["page"] == page_num]ifnot page_texts:return []# Extract bounding box centers centers = []for item in page_texts: bbox = item["bbox"]# bbox is [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] cx = np.mean([p[0] for p in bbox]) cy = np.mean([p[1] for p in bbox]) centers.append((cx, cy, item["text"]))ifnot centers:return [] centers_df = pd.DataFrame(centers, columns=["x", "y", "text"])# Cluster into rows by y-coordinate proximity centers_df = centers_df.sort_values("y") row_threshold =15# pixels centers_df["row_id"] = ( centers_df["y"].diff().abs() > row_threshold ).cumsum()# Within each row, sort by x-coordinate tables = [] rows = []for row_id, row_group in centers_df.groupby("row_id"): row_sorted = row_group.sort_values("x") rows.append(row_sorted["text"].tolist())iflen(rows) >2:# Attempt to construct DataFrame max_cols =max(len(r) for r in rows)# Pad shorter rows padded = [r + [""] * (max_cols -len(r)) for r in rows]try: df = pd.DataFrame(padded[1:], columns=padded[0]) tables.append(df)exceptException: tables.append(pd.DataFrame(padded))return tablesdef parse_financial_numbers(text):""" Parse Vietnamese financial number formats. Vietnamese uses dots as thousands separators and commas as decimals. E.g., '1.234.567' = 1234567, '1.234,56' = 1234.56 """import re text = text.strip().replace(" ", "")# Remove parentheses (negative indicator) negative = text.startswith("(") and text.endswith(")") text = text.strip("()")# Handle Vietnamese number format# If comma is present, it's a decimal separatorif","in text: text = text.replace(".", "").replace(",", ".")else: text = text.replace(".", "")try: value =float(text)return-value if negative else valueexceptValueError:return np.nan
46.3.4 Hiểu tài liệu có nhận biết bố cục
AI xử lý tài liệu hiện đại vượt ra ngoài OCR bằng cách mô hình hóa đồng thời nội dung văn bản và bố cục không gian. LayoutLM (Huang và c.s. 2022) và các mô hình kế tiếp coi mỗi token có cả vector nhúng văn bản lẫn vector nhúng vị trí được suy ra từ tọa độ khung bao của nó. Điều này cho phép mô hình hiểu rằng một con số nằm bên dưới tiêu đề “Revenue” và bên phải của “2023” là số liệu doanh thu năm 2023, ngay cả khi không có phát hiện bảng một cách tường minh.
Hiện mã
def layoutlm_extract(document_pages, model_name="layoutlmv3"):""" Extract structured financial data using LayoutLM. This function uses the pre-trained LayoutLMv3 model for document understanding with Vietnamese financial statements. Parameters ---------- document_pages : list List of (page_image, ocr_results) tuples. model_name : str Model variant. Returns ------- dict : Extracted financial fields. """from transformers import ( LayoutLMv3ForTokenClassification, LayoutLMv3Processor ) processor = LayoutLMv3Processor.from_pretrained("microsoft/layoutlmv3-base", apply_ocr=False# We provide our own OCR ) model = LayoutLMv3ForTokenClassification.from_pretrained("microsoft/layoutlmv3-base", num_labels=13# Financial statement field types )# Define target fields for extraction field_labels = ["O", # Other"B-REVENUE", "I-REVENUE","B-COGS", "I-COGS","B-NET_INCOME", "I-NET_INCOME","B-TOTAL_ASSETS", "I-TOTAL_ASSETS","B-TOTAL_EQUITY", "I-TOTAL_EQUITY","B-TOTAL_DEBT", "I-TOTAL_DEBT" ] extracted = {}for page_img, ocr_results in document_pages: words = [r["text"] for r in ocr_results] boxes = []for r in ocr_results: bbox = r["bbox"]# Normalize to 0-1000 range x0 =min(p[0] for p in bbox) y0 =min(p[1] for p in bbox) x1 =max(p[0] for p in bbox) y1 =max(p[1] for p in bbox) boxes.append([int(x0), int(y0), int(x1), int(y1)])ifnot words:continue# Process through LayoutLM encoding = processor( page_img, words, boxes=boxes, return_tensors="pt", truncation=True, max_length=512 )with torch.no_grad(): outputs = model(**encoding) predictions = outputs.logits.argmax(-1).squeeze().tolist()# Extract labeled entitiesfor idx, pred inenumerate(predictions):if pred >0and idx <len(words): label = field_labels[pred]if label.startswith("B-"): field = label[2:] value = parse_financial_numbers(words[idx])ifnot np.isnan(value): extracted[field] = valuereturn extracted
46.4 Số hóa biểu đồ và hình vẽ
46.4.1 Động cơ: Vì sao hình ảnh mang thông tin tài chính
Các biểu đồ tài chính (ví dụ: chuỗi thời gian giá, biểu đồ cột về lợi nhuận kế toán, biểu đồ phân tán về đánh đổi rủi ro-lợi nhuận) chứa thông tin mà các nhà phân tích xử lý bằng mắt. Đối với các chiến lược có hệ thống, thông tin này phải được chuyển sang dạng số. Ba trường hợp sử dụng thúc đẩy việc số hóa biểu đồ:
Khôi phục dữ liệu lịch sử. Dữ liệu tài chính thời kỳ tiền số hóa thường chỉ tồn tại trong các biểu đồ in. Việc số hóa các biểu đồ này kéo dài các chuỗi thời gian lịch sử vượt ra ngoài kỷ nguyên điện tử.
Trích xuất báo cáo của công ty môi giới. Các báo cáo phân tích bên bán chứa các biểu đồ với dự phóng và phân tích kịch bản. Việc trích xuất chúng bằng chương trình cho phép tổng hợp có hệ thống quan điểm của các nhà phân tích.
Hồ sơ quy định. Các hồ sơ quy định của Việt Nam đôi khi nhúng dữ liệu dưới dạng hình ảnh (biểu đồ, bảng quét) thay vì các giá trị máy đọc được.
46.4.2 Phân loại kiểu biểu đồ
Bước đầu tiên là phân loại kiểu biểu đồ (đường, cột, phân tán, tròn, nến), từ đó xác định thuật toán số hóa phù hợp.
Hiện mã
def build_chart_classifier(n_classes=5):""" Build a CNN-based chart type classifier. Classes: line_chart, bar_chart, scatter_plot, candlestick, pie_chart. """ model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)# Replace final layer for chart classification model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(512, n_classes) )return modeldef classify_chart(image_path, model, transform):"""Classify a chart image into one of 5 types.""" class_names = ["line_chart", "bar_chart", "scatter_plot","candlestick", "pie_chart" ] img = Image.open(image_path).convert("RGB") tensor = transform(img).unsqueeze(0)with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1).squeeze() pred_idx = probs.argmax().item()return {"predicted_class": class_names[pred_idx],"confidence": probs[pred_idx].item(),"all_probs": { name: probs[i].item()for i, name inenumerate(class_names) } }
46.4.3 Số hóa biểu đồ đường
Đối với biểu đồ đường, nhiệm vụ số hóa là khôi phục chuỗi dữ liệu \((x, y)\) từ hình ảnh. Quy trình bao gồm phát hiện trục, hiệu chỉnh thang đo và dò đường cong.
Hiện mã
def digitize_line_chart(image_path, x_range=None, y_range=None):""" Digitize a line chart image to recover the data series. Parameters ---------- image_path : str Path to chart image. x_range : tuple, optional (x_min, x_max) if known. y_range : tuple, optional (y_min, y_max) if known. Returns ------- DataFrame : Digitized data points (x, y). """import cv2 img = cv2.imread(str(image_path)) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w = gray.shape# Step 1: Detect plot area (largest rectangular region) edges = cv2.Canny(gray, 50, 150) contours, _ = cv2.findContours( edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE )if contours: largest =max(contours, key=cv2.contourArea) x_start, y_start, plot_w, plot_h = cv2.boundingRect(largest)else:# Fallback: assume plot is central 80% of image x_start, y_start =int(w *0.1), int(h *0.1) plot_w, plot_h =int(w *0.8), int(h *0.8)# Step 2: Extract line pixels within plot area# Convert to HSV and isolate colored lines hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) plot_region = hsv[y_start:y_start + plot_h, x_start:x_start + plot_w]# Detect non-white, non-gray pixels (likely the line) saturation = plot_region[:, :, 1] line_mask = saturation >30# Colored pixels# Step 3: Trace the line (column-wise median of colored pixels) data_points = []for col inrange(plot_w): col_pixels = np.where(line_mask[:, col])[0]iflen(col_pixels) >0:# Use median y-position y_pixel = np.median(col_pixels)# Convert pixel to data coordinates x_frac = col / plot_w y_frac =1- y_pixel / plot_h # Invert y-axis x_val = (x_range[0] + x_frac * (x_range[1] - x_range[0])if x_range else x_frac) y_val = (y_range[0] + y_frac * (y_range[1] - y_range[0])if y_range else y_frac) data_points.append({"x": x_val, "y": y_val})return pd.DataFrame(data_points)
46.5 Phân tích cảm xúc thị giác
46.5.1 Cảm xúc của hình ảnh trong tin tức tài chính
Các bài báo tin tức đi kèm các hình ảnh mang cảm xúc độc lập với văn bản. Bức ảnh một CEO mỉm cười tại buổi họp báo truyền tải thông tin khác với bức ảnh cùng CEO đó đối mặt với người biểu tình. Obaid và Pukthuanthong (2022) chứng minh rằng cảm xúc thị giác của các bức ảnh trên Wall Street Journal dự báo lợi nhuận thị trường: những ngày có nhiều hình ảnh tiêu cực hơn đi trước mức lợi nhuận thấp hơn.
Chúng ta triển khai phân tích cảm xúc thị giác bằng hai cách tiếp cận: một bộ phân loại cảm xúc tiền huấn luyện và một mô hình thị giác-ngôn ngữ diễn giải hình ảnh trong bối cảnh tài chính.
46.5.2 Cảm xúc thị giác dựa trên CNN
Hiện mã
def compute_visual_sentiment(image_paths, model_name="resnet50"):""" Compute visual sentiment scores using a fine-tuned CNN. Uses features from a pre-trained CNN followed by a sentiment classifier trained on the Visual Sentiment Ontology (VSO) or similar dataset. Parameters ---------- image_paths : list Paths to news images. Returns ------- DataFrame : image_path, positive_score, negative_score, sentiment. """ device ="cuda"if torch.cuda.is_available() else"cpu" model, transform, dim = build_feature_extractor(model_name, device)# Extract features features = extract_features(image_paths, model, transform, device)# Simple sentiment model: use mean activation as proxy# (In practice, fine-tune on labeled financial images)# Higher mean activation in certain feature channels# correlates with positive/negative affect# Positive channels (empirically determined via validation) pos_channels =list(range(0, dim //3)) neg_channels =list(range(dim //3, 2* dim //3)) pos_scores = features[:, pos_channels].mean(axis=1) neg_scores = features[:, neg_channels].mean(axis=1)# Normalize to [0, 1] pos_norm = (pos_scores - pos_scores.min()) / ( pos_scores.max() - pos_scores.min() +1e-8 ) neg_norm = (neg_scores - neg_scores.min()) / ( neg_scores.max() - neg_scores.min() +1e-8 ) sentiment = pos_norm - neg_normreturn pd.DataFrame({"image_path": image_paths,"positive_score": pos_norm,"negative_score": neg_norm,"net_sentiment": sentiment })
46.5.3 Mô hình thị giác-ngôn ngữ cho việc hiểu hình ảnh tài chính
Cách tiếp cận mạnh nhất cho phân tích hình ảnh tài chính sử dụng các mô hình thị giác-ngôn ngữ (VLM), xử lý đồng thời hình ảnh và văn bản. Các mô hình như CLIP (Radford và c.s. 2021), BLIP-2 (Li và c.s. 2023) và GPT-4V có thể được nhắc lệnh để diễn giải các hình ảnh tài chính trong ngữ cảnh. Chẳng hạn, với một bức ảnh chụp từ trên không của một nhà máy, một VLM có thể trả lời “Nhà máy này có đang hoạt động hết công suất không?” hoặc “Có thấy việc xây dựng thêm các cơ sở không?”
Hiện mã
def vlm_financial_analysis(image_path, prompt, model_name="clip"):""" Use a vision-language model to analyze a financial image. Parameters ---------- image_path : str Path to image. prompt : str Financial analysis prompt. model_name : str 'clip' for zero-shot classification, 'blip2' for visual question answering. Returns ------- dict : Model output (scores or text). """ img = Image.open(image_path).convert("RGB")if model_name =="clip":from transformers import CLIPProcessor, CLIPModel clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32" ) processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32" )# Zero-shot classification with financial labels labels = ["busy commercial area with many customers","empty commercial area with few customers","active construction site with workers","idle construction site without activity","healthy green crops in agricultural field","damaged or dry crops in agricultural field","busy port with many ships and containers","quiet port with few ships" ] inputs = processor( text=labels, images=img, return_tensors="pt", padding=True )with torch.no_grad(): outputs = clip_model(**inputs) logits = outputs.logits_per_image.squeeze() probs = torch.softmax(logits, dim=0) results = { label: prob.item()for label, prob inzip(labels, probs) }return {"scores": results, "top_label": max(results, key=results.get)}elif model_name =="blip2":from transformers import Blip2Processor, Blip2ForConditionalGeneration processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b" ) model = Blip2ForConditionalGeneration.from_pretrained("Salesforce/blip2-opt-2.7b", torch_dtype=torch.float16 ) inputs = processor(images=img, text=prompt, return_tensors="pt")with torch.no_grad(): generated_ids = model.generate(**inputs, max_length=100) answer = processor.decode( generated_ids[0], skip_special_tokens=True )return {"answer": answer}
46.6 Hợp nhất đa phương thức: Kết hợp hình ảnh và văn bản
46.6.1 Vì sao cần đa phương thức?
Văn bản và hình ảnh nắm bắt các khía cạnh khác nhau của cùng một thực tế kinh tế nền tảng. Một báo cáo kết quả kinh doanh mô tả hiệu quả tài chính bằng lời và số; các bức ảnh đi kèm cho thấy nhà máy, sản phẩm và ban điều hành. Một bài báo về một cảng mô tả khối lượng thương mại bằng văn bản; ảnh vệ tinh cho thấy vị trí thực tế của các tàu. Kết hợp cả hai phương thức cho ra một biểu diễn phong phú hơn so với từng phương thức riêng lẻ.
Kiến trúc hợp nhất phụ thuộc vào ứng dụng:
Hợp nhất sớm. Nối các đặc trưng hình ảnh \(\mathbf{z}^{\text{img}}\) và các đặc trưng văn bản \(\mathbf{z}^{\text{txt}}\) thành một vector duy nhất \([\mathbf{z}^{\text{img}}; \mathbf{z}^{\text{txt}}]\) trước khi dự báo. Đơn giản nhưng bỏ qua các tương tác chéo phương thức.
Hợp nhất muộn. Huấn luyện các mô hình riêng cho từng phương thức và kết hợp các dự báo: \(\hat{y} = \alpha \hat{y}^{\text{img}} + (1-\alpha) \hat{y}^{\text{txt}}\). Vững chắc nhưng không thể học các đặc trưng chéo phương thức.
Hợp nhất bằng chú ý chéo. Dùng cơ chế chú ý chéo (cross-attention) của transformer để mỗi phương thức chú ý đến phương thức còn lại. Mạnh nhất nhưng đòi hỏi nhiều dữ liệu và tính toán hơn.
46.6.2 Những cân nhắc thực tiễn đối với thị trường Việt Nam
Phân tích đa phương thức trên thị trường Việt Nam đối mặt với một số cân nhắc thực tiễn:
Căn chỉnh dữ liệu. Ảnh vệ tinh, bài báo tin tức và dữ liệu thị trường vận hành ở các tần suất thời gian và độ phân giải không gian khác nhau. Ảnh vệ tinh tổng hợp có sẵn hằng tuần hoặc hai tuần một lần; tin tức theo ngày; giao dịch trong ngày. Việc căn chỉnh đúng đòi hỏi xác định tập thông tin mà nhà đầu tư có tại thời điểm ra quyết định giao dịch để tránh thiên lệch nhìn trước.
Khan hiếm nhãn. Học có giám sát đòi hỏi dữ liệu có gán nhãn (ví dụ: hình ảnh được chú thích bằng các kết quả kinh tế). Ở Việt Nam, các nhãn thực địa (doanh số bán lẻ thực tế, năng suất cây trồng thực tế, thông lượng cảng thực tế) đến với độ trễ đáng kể và thường thiếu độ chi tiết để khớp với độ phân giải của vệ tinh. Do đó, các cách tiếp cận bán giám sát và tự giám sát là thiết yếu.
Cân nhắc về quy định. Ảnh vệ tinh độ phân giải cao của các cơ sở thương mại hoặc quân sự cụ thể có thể bị hạn chế. Các nhà nghiên cứu nên xác minh rằng các nguồn ảnh của họ tuân thủ quy định của Việt Nam về dữ liệu địa không gian.
Chi phí tính toán. Xử lý các ô ảnh vệ tinh qua CNN đòi hỏi tính toán nặng. Một ô Sentinel-2 đơn lẻ ở độ phân giải 10m bao phủ Thành phố Hồ Chí Minh chứa khoảng \(10{,}980 \times 10{,}980\) điểm ảnh trên mỗi băng tần. Việc chia thành các mảnh ảnh \(224 \times 224\) làm đầu vào cho CNN tạo ra \(\sim 2{,}400\) mảnh trên mỗi ô, mỗi mảng cần một lượt lan truyền xuôi qua mạng.
Bảng 46.4: Các nguồn dữ liệu hình ảnh cho các ứng dụng tài chính tại Việt Nam
Ứng dụng
Nguồn ảnh
Độ phân giải
Tần suất
Khả năng tiếp cận tại Việt Nam
Độ sáng ban đêm
VIIRS/DMSP
500m
Hằng tháng
Miễn phí (NOAA/EOG)
Sức khỏe cây trồng
MODIS/Sentinel-2
250m/10m
16 ngày/5 ngày
Miễn phí (NASA/ESA)
Phát hiện cảng/tàu
Sentinel-1 (SAR)
10m
12 ngày
Miễn phí (ESA Copernicus)
Giám sát xây dựng
Thương mại (Maxar)
30cm
Theo yêu cầu
Trả phí ($)
Mật độ đô thị
Sentinel-2
10m
5 ngày
Miễn phí (ESA)
OCR tài liệu
Hồ sơ của doanh nghiệp
N/A
Theo sự kiện
DataCore.vn
Ảnh tin tức
Truyền thông tài chính
N/A
Hằng ngày
Thu thập dữ liệu web
46.7 Tóm tắt
Chương này đã mở rộng bộ công cụ dữ liệu thay thế từ văn bản (chương trước) sang hình ảnh. Chúng ta đã trình bày năm lĩnh vực ứng dụng riêng biệt của dữ liệu thị giác trên thị trường tài chính Việt Nam.
Thứ nhất, ảnh vệ tinh và ảnh địa không gian cung cấp các tín hiệu kinh tế tần suất cao, chi tiết về không gian, đi trước thống kê chính thức. Độ sáng ban đêm đóng vai trò biến đại diện cho GDP cấp tỉnh với \(R^2\) theo mặt cắt ngang vượt 0.7; các chỉ số sức khỏe cây trồng NDVI dự báo lợi nhuận của các doanh nghiệp nông nghiệp; và các đặc trưng CNN được trích xuất từ các ô ảnh vệ tinh cho phép các biểu diễn không gian phong phú về hoạt động kinh tế.
Thứ hai, phân tích ảnh tài liệu giải quyết vấn đề thực tiễn là trích xuất dữ liệu có cấu trúc từ các hồ sơ tài chính của Việt Nam vốn đến dưới dạng ảnh quét. Quy trình (ví dụ: OCR với các công cụ được tối ưu cho tiếng Việt, phân tích bố cục, trích xuất bảng và hiểu tài liệu dựa trên LayoutLM) chuyển các điểm ảnh phi cấu trúc thành dữ liệu tài chính có cấu trúc mà mọi phân tích phía sau đều cần.
Thứ ba, số hóa biểu đồ khôi phục các chuỗi dữ liệu số từ các biểu diễn thị giác, mở rộng phạm vi lịch sử và cho phép sử dụng có hệ thống các sản phẩm của nhà phân tích. Thứ tư, phân tích cảm xúc thị giác từ hình ảnh tin tức cung cấp một chiều tín hiệu trực giao với cảm xúc văn bản, với sức dự báo tiềm năng đối với lợi nhuận thị trường.
Thứ năm, hợp nhất đa phương thức (kết hợp các biểu diễn hình ảnh và văn bản thông qua các kiến trúc hợp nhất sớm, muộn hoặc chú ý chéo) cho ra các mô hình dự báo phong phú hơn so với từng phương thức riêng lẻ. Lợi ích thực tiễn của các cách tiếp cận đa phương thức tăng theo mức độ đa dạng và chất lượng của dữ liệu sẵn có, khiến nó ngày càng phù hợp khi hệ sinh thái dữ liệu thay thế của Việt Nam trưởng thành.
Mạch xuyên suốt tất cả các ứng dụng là quy trình biến đổi: tensor điểm ảnh thô \(\to\) biểu diễn đặc trưng (qua CNN, ViT hoặc VLM) \(\to\) tín hiệu tài chính \(\to\) diễn giải kinh tế. Việc lựa chọn kiến trúc và chất lượng của việc thích ứng miền quyết định liệu tín hiệu thu được có nội dung dự báo thực sự hay chỉ nắm bắt nhiễu.
Baek, Jeonghun, Geewook Kim, Junyeop Lee, Sungrae Park, Dongyoon Han, Sangdoo Yun, Seong Joon Oh, và Hwalsuk Lee. 2019. “What is wrong with scene text recognition model comparisons? dataset and model analysis”. Trong Proceedings of the IEEE/CVF international conference on computer vision, 4715–23.
Donaldson, Dave. 2018. “Railroads of the Raj: Estimating the impact of transportation infrastructure”. American economic review 108 (4-5): 899–934.
Donaldson, Dave, và Adam Storeygard. 2016. “The view from above: Applications of satellite data in economics”. Journal of Economic Perspectives 30 (4): 171–98.
Dosovitskiy, Alexey, Lucas Beyer, Alexander Kolesnikov, Dirk Weissenborn, Xiaohua Zhai, Thomas Unterthiner, Mostafa Dehghani, và c.s. 2020. “An image is worth 16x16 words: Transformers for image recognition at scale”. arXiv preprint arXiv:2010.11929.
He, Kaiming, Xiangyu Zhang, Shaoqing Ren, và Jian Sun. 2016. “Deep residual learning for image recognition”. Trong Proceedings of the IEEE conference on computer vision and pattern recognition, 770–78.
Henderson, J Vernon, Tim Squires, Adam Storeygard, và David Weil. 2018. “The global distribution of economic activity: nature, history, and the role of trade”. The Quarterly Journal of Economics 133 (1): 357–406.
Henderson, J Vernon, Adam Storeygard, và David N Weil. 2012. “Measuring economic growth from outer space”. American economic review 102 (2): 994–1028.
Hodler, Roland, và Paul A Raschky. 2014. “Regional favoritism”. The Quarterly Journal of Economics 129 (2): 995–1033.
Huang, Yupan, Tengchao Lv, Lei Cui, Yutong Lu, và Furu Wei. 2022. “Layoutlmv3: Pre-training for document ai with unified text and image masking”. Trong Proceedings of the 30th ACM international conference on multimedia, 4083–91.
Jean, Neal, Marshall Burke, Michael Xie, W Matthew Alampay Davis, David B Lobell, và Stefano Ermon. 2016. “Combining satellite imagery and machine learning to predict poverty”. Science 353 (6301): 790–94.
Li, Junnan, Dongxu Li, Silvio Savarese, và Steven Hoi. 2023. “Blip-2: Bootstrapping language-image pre-training with frozen image encoders and large language models”. Trong International conference on machine learning, 19730–42. PMLR.
Obaid, Khaled, và Kuntara Pukthuanthong. 2022. “A picture is worth a thousand words: Measuring investor sentiment by combining machine learning and photos from news”. Journal of Financial Economics 144 (1): 273–97.
Radford, Alec, Jong Wook Kim, Chris Hallacy, Aditya Ramesh, Gabriel Goh, Sandhini Agarwal, Girish Sastry, và c.s. 2021. “Learning transferable visual models from natural language supervision”. Trong International conference on machine learning, 139:8748–63. Proceedings of Machine Learning Research. PMLR.
Tan, Mingxing, và Quoc Le. 2019. “Efficientnet: Rethinking model scaling for convolutional neural networks”. Trong International conference on machine learning, 6105–14. PMLR.
# Dữ liệu hình ảnh và dữ liệu thị giác trong tài chínhChương trước đã trình bày cách văn bản phi cấu trúc (ví dụ: báo cáo kết quả kinh doanh, bài báo tin tức, phần mô tả hoạt động kinh doanh) có thể được chuyển thành các tín hiệu có cấu trúc cho phân tích tài chính. Chương này mở rộng bộ công cụ dữ liệu thay thế sang một dạng thức thứ hai: hình ảnh. Dữ liệu thị giác rất dồi dào trong bối cảnh tài chính nhưng bị khai thác chưa đầy đủ một cách có hệ thống. Ảnh vệ tinh phản ánh hoạt động kinh tế thực (ví dụ: mức độ lấp đầy bãi đỗ xe tại các điểm bán lẻ, tiến độ xây dựng tại các khu công nghiệp, độ sáng ban đêm như một biến đại diện cho GDP khu vực, lưu lượng tàu tại các cảng, sức khỏe cây trồng trên các vùng nông nghiệp). Các tài liệu của doanh nghiệp đến dưới dạng PDF quét, với các bảng biểu và hình vẽ khó trích xuất bằng các phương pháp trích xuất văn bản tiêu chuẩn. Các biểu đồ tài chính mã hóa thông tin mà các nhà phân tích diễn giải bằng mắt nhưng các chiến lược có hệ thống không thể sử dụng nếu chưa được số hóa. Và nội dung thị giác của mạng xã hội, quảng cáo và hình ảnh sản phẩm mang các tín hiệu về cảm xúc và thương hiệu bổ sung cho phân tích văn bản.Thách thức cốt lõi nằm ở khâu biểu diễn: một hình ảnh là một tensor ba chiều gồm các cường độ điểm ảnh, không có cấu trúc ngữ nghĩa vốn có. Việc chuyển mảng thô này thành một tín hiệu tài chính (tức là một con số dự báo lợi nhuận, đo lường rủi ro hoặc đại diện cho hoạt động kinh tế) đòi hỏi hoặc kỹ thuật thiết kế đặc trưng thủ công, hoặc các biểu diễn được học thông qua mạng nơ-ron tích chập sâu (CNN) và vision transformer (ViT). Chương này trình bày cả hai cách tiếp cận.Chúng ta tổ chức nội dung xoay quanh năm lĩnh vực ứng dụng, mỗi lĩnh vực có nguồn dữ liệu, yêu cầu mô hình hóa và động cơ kinh tế riêng. Thứ nhất, ảnh vệ tinh và ảnh địa không gian để dự báo tức thời (nowcasting) hoạt động kinh tế. Thứ hai, phân tích ảnh tài liệu để trích xuất dữ liệu có cấu trúc từ các hồ sơ tài chính tiếng Việt. Thứ ba, số hóa biểu đồ và hình vẽ cho kiểm định ngược có hệ thống. Thứ tư, phân tích cảm xúc thị giác từ mạng xã hội và truyền thông tin tức. Thứ năm, hợp nhất đa phương thức, kết hợp tín hiệu hình ảnh và văn bản thành các mô hình dự báo chung.Thị trường Việt Nam có những cơ hội đặc thù trong lĩnh vực này. Ảnh vệ tinh đặc biệt hữu ích trong một nền kinh tế có các ngành nông nghiệp và sản xuất lớn, nơi dữ liệu thực địa đến với độ trễ đáng kể. Các hồ sơ tài chính của Việt Nam thường được phát hành dưới dạng ảnh quét thay vì định dạng máy đọc được, khiến AI xử lý tài liệu trở thành điều thiết yếu chứ không phải tùy chọn. Và quá trình đô thị hóa nhanh chóng thể hiện trong ảnh xây dựng và hạ tầng cung cấp các biến đại diện tần suất cao cho động lực kinh tế vĩ mô mà thống kê chính thức không thể sánh được.```{python}#| label: setup#| message: falseimport pandas as pdimport numpy as npfrom pathlib import Pathimport warningswarnings.filterwarnings("ignore")# Core image processingfrom PIL import Imageimport io# Deep learningimport torchimport torch.nn as nnimport torchvision.transforms as transformsimport torchvision.models as models# Visualizationimport plotnine as p9from mizani.formatters import percent_format, comma_formatimport matplotlib.pyplot as plt# Statistical analysisfrom scipy import statsimport statsmodels.api as smimport statsmodels.formula.api as smffrom linearmodels.panel import PanelOLS```## Nền tảng: Từ điểm ảnh đến tín hiệu tài chính### Biểu diễn hình ảnhMột hình ảnh số là một hàm $I: \{1, \ldots, H\} \times \{1, \ldots, W\} \times \{1, \ldots, C\} \rightarrow [0, 255]$ ánh xạ tọa độ không gian và các kênh màu thành các giá trị cường độ. Với một ảnh RGB có chiều cao $H$ và chiều rộng $W$, biểu diễn là một tensor $\mathbf{I} \in \mathbb{R}^{H \times W \times 3}$. Một ảnh RGB $224 \times 224$ đơn lẻ (tức là đầu vào tiêu chuẩn của các CNN hiện đại) chứa $224 \times 224 \times 3 = 150{,}528$ chiều. Số chiều cực lớn này, kết hợp với cấu trúc không gian (các điểm ảnh lân cận tương quan với nhau), khiến hình ảnh khác biệt căn bản so với dữ liệu tài chính dạng bảng và đòi hỏi các kiến trúc chuyên biệt.Điểm mấu chốt của mạng nơ-ron tích chập là chia sẻ tham số thông qua các bộ lọc cục bộ. Một lớp tích chập áp dụng một nhân $\mathbf{K} \in \mathbb{R}^{k \times k \times C_{\text{in}}}$ để tạo ra một bản đồ đặc trưng:$$(\mathbf{I} * \mathbf{K})(i, j) = \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} \sum_{c=1}^{C_{\text{in}}} I(i+m, j+n, c) \cdot K(m, n, c)$$ {#eq-convolution}Bằng cách xếp chồng các lớp tích chập với các hàm phi tuyến và các phép gộp (pooling), mạng xây dựng một hệ thống phân cấp các biểu diễn: các lớp đầu phát hiện cạnh và kết cấu; các lớp giữa phát hiện các bộ phận và mẫu hình; các lớp sâu phát hiện đối tượng và cảnh vật. Đầu ra của lớp cuối $\mathbf{z} \in \mathbb{R}^{d}$ (với $d$ thường là 512-2048) là một biểu diễn gọn của nội dung ngữ nghĩa của hình ảnh, có thể được dùng trực tiếp làm vector đặc trưng cho dự báo tài chính.Hình ảnh tài chính trải rộng trên nhiều độ phân giải và dạng thức:| Nguồn | Độ phân giải | Kênh | Kích thước điển hình | Tần suất cập nhật ||---------------|---------------|---------------|---------------|---------------|| Vệ tinh Sentinel-2 | 10m/pixel | 13 băng tần | 10,980 × 10,980 | 5 ngày || Planet Labs | 3m/pixel | 4 băng tần | 4,000 × 4,000 | Hằng ngày || Độ sáng ban đêm VIIRS | 500m/pixel | 1 (DNB) | 3,000 × 1,800 | Tổng hợp hằng tháng || Bản quét báo cáo thường niên | 300 DPI | 3 (RGB) | 2,480 × 3,508 | Hằng năm || Ảnh chân dung CEO | Thay đổi | 3 (RGB) | 500 × 500 | Hằng năm || Ảnh tin tức | Thay đổi | 3 (RGB) | 800 × 600 | Thời gian thực || Biểu đồ tài chính | Thay đổi | 3 (RGB) | 1,000 × 600 | Thời gian thực |: Các nguồn dữ liệu hình ảnh cho thị trường tài chính Việt Nam {#tbl-image-sources}### Học chuyển giao cho tài chínhHuấn luyện một CNN từ đầu đòi hỏi hàng triệu ảnh có gán nhãn, nhiều hơn rất nhiều so với bất kỳ ứng dụng tài chính nào có thể cung cấp. Học chuyển giao (transfer learning) giải quyết vấn đề này bằng cách dùng các mạng đã được tiền huấn luyện trên ImageNet (1,2 triệu ảnh, 1.000 lớp) làm bộ trích xuất đặc trưng. Mạng tiền huấn luyện đã học được các biểu diễn thị giác chung (cạnh, kết cấu, hình dạng, đối tượng); chúng ta chỉ cần thay lớp phân loại cuối cùng bằng một lớp đầu ra (head) riêng cho tác vụ.Về mặt hình thức, gọi $f_{\boldsymbol{\theta}}(\mathbf{I})$ là một mạng tiền huấn luyện với các tham số $\boldsymbol{\theta}$ được chia thành bộ trích xuất đặc trưng $\boldsymbol{\theta}_{\text{feat}}$ và bộ phân loại $\boldsymbol{\theta}_{\text{cls}}$. Đối với các ứng dụng tài chính, chúng ta:1. **Trích xuất đặc trưng**: Đóng băng $\boldsymbol{\theta}_{\text{feat}}$, trích xuất $\mathbf{z} = f_{\boldsymbol{\theta}_{\text{feat}}}(\mathbf{I})$, và huấn luyện một mô hình đơn giản (hồi quy tuyến tính, gradient boosting) trên $\mathbf{z}$.2. **Tinh chỉnh**: Khởi tạo từ $\boldsymbol{\theta}$ và huấn luyện tất cả các tham số trên tác vụ tài chính với tốc độ học nhỏ để tránh quên thảm khốc (catastrophic forgetting).Các họ kiến trúc chính là:**ResNet** \[@he2016deep\]. Các kết nối phần dư ($y = F(x) + x$) cho phép huấn luyện các mạng rất sâu (50-152 lớp). Kết nối tắt giải quyết vấn đề triệt tiêu gradient. ResNet-50 tạo ra một vector đặc trưng 2.048 chiều từ lớp áp chót.**EfficientNet** \[@tan2019efficientnet\]. Mở rộng kết hợp (compound scaling) đồng thời độ sâu, độ rộng và độ phân giải. EfficientNet-B0 đạt độ chính xác của ResNet-50 với 5,3 triệu tham số (so với 25,6 triệu), khiến nó khả thi để xử lý hàng nghìn ô ảnh vệ tinh.**Vision Transformer (ViT)** \[@dosovitskiy2020image\]. Xem một hình ảnh như một chuỗi các mảnh ảnh (patch) $16 \times 16$, xử lý chúng qua một bộ mã hóa Transformer tiêu chuẩn. ViT-B/16 tạo ra một vector nhúng 768 chiều. Đặc biệt hiệu quả đối với ảnh tài liệu, nơi các quan hệ không gian giữa các thành phần (bảng, tiêu đề, khối văn bản) đóng vai trò quan trọng.```{python}#| label: feature-extractor#| eval: false# DataCore.vn APIfrom datacore import DataCoredc = DataCore()def build_feature_extractor(model_name="resnet50", device="cpu"):""" Build a pre-trained CNN feature extractor. Parameters ---------- model_name : str One of 'resnet50', 'efficientnet_b0', 'vit_b_16'. device : str 'cpu' or 'cuda'. Returns ------- model : nn.Module Feature extraction model. transform : transforms.Compose Image preprocessing pipeline. """if model_name =="resnet50": weights = models.ResNet50_Weights.IMAGENET1K_V2 model = models.resnet50(weights=weights) model.fc = nn.Identity() # Remove classification head dim =2048elif model_name =="efficientnet_b0": weights = models.EfficientNet_B0_Weights.IMAGENET1K_V1 model = models.efficientnet_b0(weights=weights) model.classifier = nn.Identity() dim =1280elif model_name =="vit_b_16": weights = models.ViT_B_16_Weights.IMAGENET1K_V1 model = models.vit_b_16(weights=weights) model.heads = nn.Identity() dim =768 model = model.to(device).eval() transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])return model, transform, dimdef extract_features(image_paths, model, transform, device="cpu", batch_size=32):""" Extract deep features from a list of images. Parameters ---------- image_paths : list Paths to image files. model : nn.Module Feature extraction model. transform : transforms.Compose Preprocessing pipeline. Returns ------- np.ndarray : Feature matrix (n_images x feature_dim). """ features = []for i inrange(0, len(image_paths), batch_size): batch_paths = image_paths[i:i + batch_size] batch_tensors = []for path in batch_paths:try: img = Image.open(path).convert("RGB") tensor = transform(img) batch_tensors.append(tensor)exceptException: batch_tensors.append(torch.zeros(3, 224, 224)) batch = torch.stack(batch_tensors).to(device)with torch.no_grad(): batch_features = model(batch).cpu().numpy() features.append(batch_features)return np.vstack(features)```## Ảnh vệ tinh và ảnh địa không gian### Hoạt động kinh tế nhìn từ không gianẢnh vệ tinh cung cấp các phép đo hoạt động kinh tế có tần suất cao và chi tiết về không gian, độc lập với và thường đi trước thống kê chính thức. Công trình nền tảng của @henderson2012measuring đã chứng minh rằng độ sáng ban đêm, được đo bởi Chương trình Vệ tinh Khí tượng Quốc phòng (DMSP), là một biến đại diện đáng tin cậy cho GDP, đặc biệt ở những quốc gia có thống kê chính thức nhiều nhiễu hoặc chậm trễ. @donaldson2016view khảo cứu các ứng dụng rộng rãi của dữ liệu vệ tinh trong kinh tế học, từ năng suất nông nghiệp đến tăng trưởng đô thị và đo lường xung đột. @donaldson2018railroads dùng các thước đo sản lượng nông nghiệp lấy từ vệ tinh để nghiên cứu lợi ích phúc lợi của đường sắt ở Ấn Độ thuộc địa. @jean2016combining kết hợp ảnh vệ tinh ban ngày với các CNN để dự đoán nghèo đói từ không gian với $r^2 > 0.7$.Tài liệu kinh tế chính trị khai thác dữ liệu vệ tinh để trả lời những câu hỏi không thể giải đáp bằng thống kê chính thức. @hodler2014regional dùng độ sáng ban đêm để cho thấy các vùng dưới cấp quốc gia có liên kết chính trị với nhà lãnh đạo quốc gia có mức hoạt động kinh tế cao hơn hẳn so với các vùng khác (ví dụ: một phát hiện về sự thiên vị theo vùng có liên quan trực tiếp đến việc phân bổ đầu tư cấp tỉnh của Việt Nam). @henderson2018global mở rộng khung độ sáng để nghiên cứu phân bố toàn cầu của hoạt động kinh tế, cho thấy địa lý, lịch sử và thương mại cùng quyết định hình thái phân bố không gian của sự thịnh vượng.Đối với các ứng dụng tài chính, điểm mấu chốt là dữ liệu vệ tinh đến nhanh hơn lợi nhuận kế toán của doanh nghiệp hay thống kê của chính phủ. Doanh thu hằng quý của một nhà bán lẻ được báo cáo 4-8 tuần sau khi quý kết thúc; ảnh vệ tinh về các bãi đỗ xe của họ có sẵn trong vòng vài ngày. Lợi thế về thời gian này tạo ra một trường hợp sử dụng tự nhiên cho dự báo tức thời (tức là ước tính các điều kiện kinh tế hiện tại trước khi dữ liệu chính thức được công bố) và cho việc xây dựng các tín hiệu giao dịch dựa trên thông tin công khai nhưng tốn kém để xử lý.### Ứng dụng 1: Độ sáng ban đêm và GDP cấp tỉnhTổng cục Thống kê (GSO) của Việt Nam công bố GDP cấp tỉnh với độ trễ vài tháng. Độ sáng ban đêm từ cảm biến VIIRS (Visible Infrared Imaging Radiometer Suite) cung cấp một giải pháp thay thế gần như theo thời gian thực. Chúng ta xây dựng một thước đo mức độ phơi nhiễm ở cấp doanh nghiệp bằng cách liên kết địa điểm đăng ký của mỗi doanh nghiệp niêm yết với độ sáng của tỉnh đó.```{python}#| label: nightlight-data#| eval: false# Load nighttime luminosity data (VIIRS monthly composites)# Source: Earth Observation Group (EOG) / NOAAnightlights = dc.get_nightlight_data( start_date="2014-01-01", end_date="2024-12-31", resolution="province")# Load firm location datafirm_locations = dc.get_firm_locations()# Provincial GDP from GSOprovincial_gdp = dc.get_provincial_gdp( start_date="2014-01-01", end_date="2024-12-31")print(f"Nightlight observations: {len(nightlights)}")print(f"Provinces covered: {nightlights['province'].nunique()}")print(f"Firms with location: {firm_locations['ticker'].nunique()}")``````{python}#| label: nightlight-gdp-validation#| eval: false# Validate: does nightlight predict provincial GDP?nl_gdp = nightlights.merge( provincial_gdp, on=["province", "year", "quarter"], how="inner")# Log-log specification (standard in the literature)nl_gdp["ln_luminosity"] = np.log(nl_gdp["mean_radiance"].clip(lower=0.01))nl_gdp["ln_gdp"] = np.log(nl_gdp["provincial_gdp"].clip(lower=1))# Cross-sectional regression by yearvalidation_results = []for year in nl_gdp["year"].unique(): subset = nl_gdp[nl_gdp["year"] == year]iflen(subset) <20:continue model = sm.OLS( subset["ln_gdp"], sm.add_constant(subset["ln_luminosity"]) ).fit() validation_results.append({"year": year,"beta": model.params.iloc[1],"r_squared": model.rsquared,"n_provinces": int(model.nobs) })validation_df = pd.DataFrame(validation_results)print(f"Avg R² (ln GDP ~ ln Luminosity): {validation_df['r_squared'].mean():.3f}")``````{python}#| label: fig-nightlight-gdp#| eval: false#| fig-cap: "Độ sáng ban đêm như một biến dự báo GDP cấp tỉnh"( p9.ggplot(nl_gdp[nl_gdp["year"] ==2023], p9.aes(x="ln_luminosity", y="ln_gdp"))+ p9.geom_point(color="#2E5090", alpha=0.6, size=2)+ p9.geom_smooth(method="lm", color="#C0392B", se=True, size=0.8)+ p9.labs( x="ln(Mean Nighttime Radiance)", y="ln(Provincial GDP)", title="Nighttime Luminosity vs. Provincial GDP (2023)" )+ p9.theme_minimal()+ p9.theme(figure_size=(10, 6)))``````{python}#| label: nightlight-firm-signal#| eval: false# Construct firm-level nightlight signal# Logic: firms in provinces with accelerating luminosity# are experiencing positive local economic conditionsnl_growth = nightlights.copy().sort_values(["province", "year", "quarter"])# Year-over-year luminosity growth by provincenl_growth["ln_radiance"] = np.log(nl_growth["mean_radiance"].clip(lower=0.01))nl_growth["ln_radiance_lag4"] = nl_growth.groupby("province")["ln_radiance"].shift(4)nl_growth["nl_growth"] = nl_growth["ln_radiance"] - nl_growth["ln_radiance_lag4"]# Merge with firms via provincefirm_nl = firm_locations[["ticker", "province"]].merge( nl_growth[["province", "year", "quarter", "nl_growth"]], on="province", how="inner")# Merge with stock returnsmonthly_returns = dc.get_monthly_returns( start_date="2014-01-01", end_date="2024-12-31")monthly_returns["year"] = monthly_returns["date"].dt.yearmonthly_returns["quarter"] = monthly_returns["date"].dt.quarterreturns_with_nl = monthly_returns.merge( firm_nl, on=["ticker", "year", "quarter"], how="inner")``````{python}#| label: nightlight-portfolio#| eval: false# Portfolio sort: quintiles on provincial nightlight growthreturns_with_nl["nl_quintile"] = returns_with_nl.groupby("date")["nl_growth"].transform(lambda x: pd.qcut(x, 5, labels=[1, 2, 3, 4, 5], duplicates="drop"))nl_port_returns = ( returns_with_nl.groupby(["date", "nl_quintile"]) .agg(port_ret=("ret", "mean")) .reset_index())nl_wide = nl_port_returns.pivot( index="date", columns="nl_quintile", values="port_ret")nl_wide["L-S"] = nl_wide[5] - nl_wide[1] # High NL growth - Low``````{python}#| label: tbl-nightlight-portfolios#| eval: false#| tbl-cap: "Lợi nhuận của danh mục theo nhóm ngũ phân vị tăng trưởng độ sáng ban đêm"nl_summary = nl_wide.describe().T[["mean", "std"]].copy()nl_summary["mean_ann"] = nl_summary["mean"] *12nl_summary["sharpe"] = ( nl_summary["mean_ann"] / (nl_summary["std"] * np.sqrt(12)))for col in nl_wide.columns: t_stat = nl_wide[col].mean() / ( nl_wide[col].std() / np.sqrt(len(nl_wide.dropna())) ) nl_summary.loc[col, "t_stat"] = t_statnl_summary = nl_summary[["mean_ann", "sharpe", "t_stat"]].round(4)nl_summary.columns = ["Ann. Return", "Sharpe", "t-stat"]nl_summary```### Ứng dụng 2: Ảnh vệ tinh cho dự báo tức thời theo ngànhNgoài độ sáng ban đêm, ảnh vệ tinh ban ngày cung cấp các tín hiệu đặc thù theo ngành. Chúng ta triển khai ba kênh liên quan đến nền kinh tế Việt Nam.**Port activity.** Vietnam is a major export-oriented economy. Satellite imagery of container ports (Cát Lái, Hải Phòng) captures trade throughput before customs statistics are released. Ship detection algorithms applied to synthetic aperture radar (SAR) imagery count vessels and estimate cargo volumes.**Tiến độ xây dựng.** Bất động sản và xây dựng chiếm một tỷ trọng đáng kể trong GDP và vốn hóa thị trường của Việt Nam. Các thuật toán phát hiện thay đổi áp dụng cho ảnh quang học độ phân giải cao nhận diện các công trình khởi công, tỷ lệ hoàn thành và việc chuyển đổi mục đích sử dụng đất.**Giám sát nông nghiệp.** Việt Nam là nước xuất khẩu hàng đầu về gạo, cà phê, cao su và thủy sản. Chỉ số khác biệt thực vật chuẩn hóa (NDVI), được tính từ dữ liệu vệ tinh đa phổ, cung cấp đánh giá về sức khỏe cây trồng:$$\text{NDVI} = \frac{\rho_{\text{NIR}} - \rho_{\text{Red}}}{\rho_{\text{NIR}} + \rho_{\text{Red}}}$$ {#eq-ndvi}trong đó $\rho_{\text{NIR}}$ và $\rho_{\text{Red}}$ là độ phản xạ ở các băng cận hồng ngoại và băng đỏ. NDVI nhận giá trị từ $-1$ đến $+1$, với các giá trị trên 0,3 cho thấy thực vật khỏe mạnh. Các độ lệch so với chuẩn theo mùa là biến đại diện cho các bất ngờ về năng suất cây trồng.```{python}#| label: ndvi-agriculture#| eval: false# Load NDVI data for Vietnamese agricultural regions# Source: MODIS/Terra (MOD13Q1, 250m resolution, 16-day composites)ndvi_data = dc.get_ndvi_data( start_date="2014-01-01", end_date="2024-12-31", regions=["mekong_delta", "central_highlands","red_river_delta", "southeast"])# Compute NDVI anomaly: deviation from 5-year seasonal averagendvi_data["month"] = ndvi_data["date"].dt.monthseasonal_mean = ( ndvi_data.groupby(["region", "month"]) ["mean_ndvi"].transform(lambda x: x.rolling(5*12, min_periods=12).mean() ))ndvi_data["ndvi_anomaly"] = ndvi_data["mean_ndvi"] - seasonal_mean# Agricultural sector firmsagri_firms = dc.get_firms_by_sector(sector="agriculture")# Link NDVI anomaly to agricultural firm returnsagri_returns = monthly_returns[ monthly_returns["ticker"].isin(agri_firms["ticker"])].copy()agri_returns["month"] = agri_returns["date"].dt.monthagri_returns["year"] = agri_returns["date"].dt.year# Regional NDVI aggregation (Mekong Delta for rice firms, etc.)mekong_ndvi = ndvi_data[ndvi_data["region"] =="mekong_delta"].copy()mekong_ndvi["year"] = mekong_ndvi["date"].dt.yearmekong_ndvi["month"] = mekong_ndvi["date"].dt.monthmekong_monthly = ( mekong_ndvi.groupby(["year", "month"]) .agg(ndvi_anomaly=("ndvi_anomaly", "mean")) .reset_index())``````{python}#| label: fig-ndvi-timeseries#| eval: false#| fig-cap: "Bất thường NDVI ở Đồng bằng sông Cửu Long: tín hiệu sức khỏe cây trồng"ndvi_plot = ndvi_data[ndvi_data["region"] =="mekong_delta"].copy()( p9.ggplot(ndvi_plot, p9.aes(x="date", y="ndvi_anomaly"))+ p9.geom_line(color="#27AE60", alpha=0.5, size=0.4)+ p9.geom_smooth(method="lowess", color="#2E5090", size=1, se=False)+ p9.geom_hline(yintercept=0, linetype="dashed", color="gray")+ p9.labs( x="", y="NDVI Anomaly", title="Mekong Delta Vegetation Health: Deviation from Seasonal Norm" )+ p9.theme_minimal()+ p9.theme(figure_size=(12, 5)))``````{python}#| label: ndvi-return-prediction#| eval: false# Panel regression: agricultural firm returns on NDVI anomalyagri_panel = agri_returns.merge( mekong_monthly, on=["year", "month"], how="inner")# Lagged NDVI anomaly (one month)agri_panel = agri_panel.sort_values(["ticker", "date"])agri_panel["ndvi_lag1"] = agri_panel.groupby("ticker")["ndvi_anomaly"].shift(1)agri_clean = agri_panel.dropna( subset=["ret", "ndvi_lag1"]).set_index(["ticker", "date"])model_ndvi = PanelOLS( agri_clean["ret"], agri_clean[["ndvi_lag1"]], entity_effects=True, time_effects=True, check_rank=False).fit(cov_type="clustered", cluster_entity=True)agri_clean = agri_clean.reset_index()print(f"NDVI → Agricultural Returns:")print(f" β(NDVI_lag): {model_ndvi.params['ndvi_lag1']:.4f}")print(f" t-stat: {model_ndvi.tstats['ndvi_lag1']:.3f}")print(f" R² (within): {model_ndvi.rsquared_within:.4f}")```### Trích xuất đặc trưng từ ảnh vệ tinh bằng CNNĐối với ảnh vệ tinh thô (thay vì các chỉ số tính sẵn như NDVI), chúng ta dùng học chuyển giao từ các CNN để trích xuất các đặc trưng không gian. Cách tiếp cận theo @jean2016combining: dùng một CNN đã tiền huấn luyện trên ImageNet để trích xuất các vector đặc trưng từ các ô ảnh vệ tinh, sau đó hồi quy các kết quả kinh tế theo các đặc trưng này.```{python}#| label: satellite-cnn-pipeline#| eval: falsedef satellite_feature_pipeline(image_dir, model_name="resnet50"):""" Extract CNN features from satellite image tiles. Parameters ---------- image_dir : str or Path Directory containing satellite tiles (PNG/TIFF). model_name : str Pre-trained model to use. Returns ------- DataFrame : image_id, feature vector columns. """ image_dir = Path(image_dir) image_paths =sorted(image_dir.glob("*.png")) +sorted( image_dir.glob("*.tif") )ifnot image_paths:print("No images found.")return pd.DataFrame() device ="cuda"if torch.cuda.is_available() else"cpu" model, transform, dim = build_feature_extractor(model_name, device) features = extract_features(image_paths, model, transform, device)# Create DataFrame feature_cols = [f"feat_{i}"for i inrange(dim)] df = pd.DataFrame(features, columns=feature_cols) df["image_id"] = [p.stem for p in image_paths]return dfdef predict_economic_activity(features_df, labels_df, label_col, n_components=50):""" Predict economic activity from satellite image features. Uses PCA for dimensionality reduction, then ridge regression. Parameters ---------- features_df : DataFrame CNN features with image_id. labels_df : DataFrame Economic outcomes with image_id. label_col : str Target variable column name. n_components : int PCA components to retain. Returns ------- dict : R², coefficients, cross-validated performance. """from sklearn.decomposition import PCAfrom sklearn.linear_model import RidgeCVfrom sklearn.model_selection import cross_val_score merged = features_df.merge(labels_df, on="image_id") feature_cols = [c for c in features_df.columns if c.startswith("feat_")] X = merged[feature_cols].values y = merged[label_col].values# PCA pca = PCA(n_components=n_components) X_pca = pca.fit_transform(X) var_explained = pca.explained_variance_ratio_.sum()# Ridge regression with cross-validation ridge = RidgeCV(alphas=np.logspace(-3, 3, 20), cv=5) cv_scores = cross_val_score(ridge, X_pca, y, cv=5, scoring="r2") ridge.fit(X_pca, y)return {"r2_cv_mean": cv_scores.mean(),"r2_cv_std": cv_scores.std(),"r2_train": ridge.score(X_pca, y),"pca_var_explained": var_explained,"optimal_alpha": ridge.alpha_,"n_images": len(merged) }```## Phân tích ảnh tài liệu### Vấn đề hồ sơ tài chính của Việt NamMột tỷ lệ đáng kể các bản công bố thông tin của doanh nghiệp Việt Nam (ví dụ: báo cáo thường niên, báo cáo tài chính, nghị quyết hội đồng quản trị, biên bản họp cổ đông) được phát hành dưới dạng ảnh PDF quét thay vì văn bản máy đọc được. Điều này tạo ra một nút thắt trong trích xuất dữ liệu: thông tin tồn tại nhưng bị mắc kẹt trong định dạng điểm ảnh. Không như các hồ sơ ở những thị trường phát triển hơn (nơi các quy định bắt buộc XBRL bảo đảm khả năng máy đọc được), các hồ sơ của Việt Nam đòi hỏi nhận dạng ký tự quang học (OCR) và phân tích bố cục trước khi có thể bắt đầu bất kỳ phân tích định lượng nào.Quy trình AI xử lý tài liệu cho các hồ sơ tài chính Việt Nam gồm bốn giai đoạn:1. **Phân loại trang**: Xác định những trang nào chứa báo cáo tài chính, thảo luận của ban điều hành, ý kiến kiểm toán, v.v.2. **Phân tích bố cục**: Phát hiện cấu trúc không gian như tiêu đề, đoạn văn, bảng, hình vẽ, chú thích.3. **OCR**: Chuyển các vùng ảnh thành văn bản, dùng các mô hình được tối ưu cho tiếng Việt.4. **Trích xuất có cấu trúc**: Phân tích cú pháp văn bản đã nhận dạng thành dữ liệu có cấu trúc (ví dụ: số liệu doanh thu, các khoản mục bảng cân đối kế toán).### OCR cho tài liệu tài chính tiếng ViệtStandard OCR engines (Tesseract, Google Cloud Vision) struggle with Vietnamese financial documents due to the combination of Vietnamese diacritics (ă, ơ, ư, ê, etc.), mixed Vietnamese-English content, and complex table layouts. We implement a pipeline using PaddleOCR (which has strong CJK and Southeast Asian language support) and VietOCR (a Vietnamese-specific model based on the transformer architecture of @baek2019wrong).```{python}#| label: ocr-pipeline#| eval: falsedef ocr_financial_document(pdf_path, language="vi", engine="paddleocr"):""" OCR a Vietnamese financial document (scanned PDF). Parameters ---------- pdf_path : str Path to PDF file. language : str Language code. engine : str 'paddleocr' or 'vietocr'. Returns ------- list[dict] : Per-page OCR results with bounding boxes. """from pdf2image import convert_from_path# Convert PDF pages to images pages = convert_from_path(pdf_path, dpi=300) results = []if engine =="paddleocr":from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="vi", use_gpu=False)for page_num, page_img inenumerate(pages):# Convert PIL to numpy img_array = np.array(page_img) ocr_result = ocr.ocr(img_array, cls=True) page_texts = []for line in ocr_result[0]: bbox, (text, confidence) = line page_texts.append({"text": text,"confidence": confidence,"bbox": bbox,"page": page_num +1 }) results.extend(page_texts)return resultsdef classify_page_type(ocr_results, page_num):""" Classify a document page by content type using keyword matching. Returns one of: 'balance_sheet', 'income_statement', 'cash_flow', 'notes', 'audit', 'management', 'other'. """ page_text =" ".join( [r["text"] for r in ocr_results if r["page"] == page_num] ).lower()# Vietnamese financial statement keywords keyword_map = {"balance_sheet": ["bảng cân đối kế toán", "tài sản", "nguồn vốn","nợ phải trả", "vốn chủ sở hữu" ],"income_statement": ["kết quả hoạt động kinh doanh", "doanh thu","lợi nhuận", "chi phí", "thu nhập" ],"cash_flow": ["lưu chuyển tiền tệ", "dòng tiền","hoạt động kinh doanh", "hoạt động đầu tư" ],"audit": ["báo cáo kiểm toán", "kiểm toán viên","ý kiến kiểm toán", "trung thực và hợp lý" ],"management": ["ban giám đốc", "hội đồng quản trị","báo cáo thường niên", "tình hình hoạt động" ] } scores = {}for page_type, keywords in keyword_map.items(): scores[page_type] =sum(1for kw in keywords if kw in page_text )ifmax(scores.values()) ==0:return"other"returnmax(scores, key=scores.get)```### Trích xuất bảng từ báo cáo tài chínhTác vụ trích xuất có giá trị cao nhất là khôi phục các bảng có cấu trúc từ báo cáo tài chính. Chúng ta triển khai một cách tiếp cận hai giai đoạn: trước tiên phát hiện các vùng bảng bằng một mô hình phân tích bố cục, sau đó phân tích cú pháp các vùng được phát hiện thành cấu trúc hàng-cột.```{python}#| label: table-extraction#| eval: falsedef extract_tables_from_page(page_image, ocr_results, page_num):""" Extract structured tables from a document page. Uses spatial clustering of OCR bounding boxes to identify table regions, then aligns text into rows and columns. Parameters ---------- page_image : PIL.Image Page image. ocr_results : list[dict] OCR results for this page. page_num : int Page number. Returns ------- list[pd.DataFrame] : Extracted tables as DataFrames. """ page_texts = [r for r in ocr_results if r["page"] == page_num]ifnot page_texts:return []# Extract bounding box centers centers = []for item in page_texts: bbox = item["bbox"]# bbox is [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] cx = np.mean([p[0] for p in bbox]) cy = np.mean([p[1] for p in bbox]) centers.append((cx, cy, item["text"]))ifnot centers:return [] centers_df = pd.DataFrame(centers, columns=["x", "y", "text"])# Cluster into rows by y-coordinate proximity centers_df = centers_df.sort_values("y") row_threshold =15# pixels centers_df["row_id"] = ( centers_df["y"].diff().abs() > row_threshold ).cumsum()# Within each row, sort by x-coordinate tables = [] rows = []for row_id, row_group in centers_df.groupby("row_id"): row_sorted = row_group.sort_values("x") rows.append(row_sorted["text"].tolist())iflen(rows) >2:# Attempt to construct DataFrame max_cols =max(len(r) for r in rows)# Pad shorter rows padded = [r + [""] * (max_cols -len(r)) for r in rows]try: df = pd.DataFrame(padded[1:], columns=padded[0]) tables.append(df)exceptException: tables.append(pd.DataFrame(padded))return tablesdef parse_financial_numbers(text):""" Parse Vietnamese financial number formats. Vietnamese uses dots as thousands separators and commas as decimals. E.g., '1.234.567' = 1234567, '1.234,56' = 1234.56 """import re text = text.strip().replace(" ", "")# Remove parentheses (negative indicator) negative = text.startswith("(") and text.endswith(")") text = text.strip("()")# Handle Vietnamese number format# If comma is present, it's a decimal separatorif","in text: text = text.replace(".", "").replace(",", ".")else: text = text.replace(".", "")try: value =float(text)return-value if negative else valueexceptValueError:return np.nan```### Hiểu tài liệu có nhận biết bố cụcAI xử lý tài liệu hiện đại vượt ra ngoài OCR bằng cách mô hình hóa đồng thời nội dung văn bản và bố cục không gian. LayoutLM [@huang2022layoutlmv3] và các mô hình kế tiếp coi mỗi token có cả vector nhúng văn bản lẫn vector nhúng vị trí được suy ra từ tọa độ khung bao của nó. Điều này cho phép mô hình hiểu rằng một con số nằm bên dưới tiêu đề "Revenue" và bên phải của "2023" là số liệu doanh thu năm 2023, ngay cả khi không có phát hiện bảng một cách tường minh.```{python}#| label: layoutlm-extraction#| eval: falsedef layoutlm_extract(document_pages, model_name="layoutlmv3"):""" Extract structured financial data using LayoutLM. This function uses the pre-trained LayoutLMv3 model for document understanding with Vietnamese financial statements. Parameters ---------- document_pages : list List of (page_image, ocr_results) tuples. model_name : str Model variant. Returns ------- dict : Extracted financial fields. """from transformers import ( LayoutLMv3ForTokenClassification, LayoutLMv3Processor ) processor = LayoutLMv3Processor.from_pretrained("microsoft/layoutlmv3-base", apply_ocr=False# We provide our own OCR ) model = LayoutLMv3ForTokenClassification.from_pretrained("microsoft/layoutlmv3-base", num_labels=13# Financial statement field types )# Define target fields for extraction field_labels = ["O", # Other"B-REVENUE", "I-REVENUE","B-COGS", "I-COGS","B-NET_INCOME", "I-NET_INCOME","B-TOTAL_ASSETS", "I-TOTAL_ASSETS","B-TOTAL_EQUITY", "I-TOTAL_EQUITY","B-TOTAL_DEBT", "I-TOTAL_DEBT" ] extracted = {}for page_img, ocr_results in document_pages: words = [r["text"] for r in ocr_results] boxes = []for r in ocr_results: bbox = r["bbox"]# Normalize to 0-1000 range x0 =min(p[0] for p in bbox) y0 =min(p[1] for p in bbox) x1 =max(p[0] for p in bbox) y1 =max(p[1] for p in bbox) boxes.append([int(x0), int(y0), int(x1), int(y1)])ifnot words:continue# Process through LayoutLM encoding = processor( page_img, words, boxes=boxes, return_tensors="pt", truncation=True, max_length=512 )with torch.no_grad(): outputs = model(**encoding) predictions = outputs.logits.argmax(-1).squeeze().tolist()# Extract labeled entitiesfor idx, pred inenumerate(predictions):if pred >0and idx <len(words): label = field_labels[pred]if label.startswith("B-"): field = label[2:] value = parse_financial_numbers(words[idx])ifnot np.isnan(value): extracted[field] = valuereturn extracted```## Số hóa biểu đồ và hình vẽ### Động cơ: Vì sao hình ảnh mang thông tin tài chínhCác biểu đồ tài chính (ví dụ: chuỗi thời gian giá, biểu đồ cột về lợi nhuận kế toán, biểu đồ phân tán về đánh đổi rủi ro-lợi nhuận) chứa thông tin mà các nhà phân tích xử lý bằng mắt. Đối với các chiến lược có hệ thống, thông tin này phải được chuyển sang dạng số. Ba trường hợp sử dụng thúc đẩy việc số hóa biểu đồ:1. **Khôi phục dữ liệu lịch sử.** Dữ liệu tài chính thời kỳ tiền số hóa thường chỉ tồn tại trong các biểu đồ in. Việc số hóa các biểu đồ này kéo dài các chuỗi thời gian lịch sử vượt ra ngoài kỷ nguyên điện tử.2. **Trích xuất báo cáo của công ty môi giới.** Các báo cáo phân tích bên bán chứa các biểu đồ với dự phóng và phân tích kịch bản. Việc trích xuất chúng bằng chương trình cho phép tổng hợp có hệ thống quan điểm của các nhà phân tích.3. **Hồ sơ quy định.** Các hồ sơ quy định của Việt Nam đôi khi nhúng dữ liệu dưới dạng hình ảnh (biểu đồ, bảng quét) thay vì các giá trị máy đọc được.### Phân loại kiểu biểu đồBước đầu tiên là phân loại kiểu biểu đồ (đường, cột, phân tán, tròn, nến), từ đó xác định thuật toán số hóa phù hợp.```{python}#| label: chart-classification#| eval: falsedef build_chart_classifier(n_classes=5):""" Build a CNN-based chart type classifier. Classes: line_chart, bar_chart, scatter_plot, candlestick, pie_chart. """ model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)# Replace final layer for chart classification model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(512, n_classes) )return modeldef classify_chart(image_path, model, transform):"""Classify a chart image into one of 5 types.""" class_names = ["line_chart", "bar_chart", "scatter_plot","candlestick", "pie_chart" ] img = Image.open(image_path).convert("RGB") tensor = transform(img).unsqueeze(0)with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1).squeeze() pred_idx = probs.argmax().item()return {"predicted_class": class_names[pred_idx],"confidence": probs[pred_idx].item(),"all_probs": { name: probs[i].item()for i, name inenumerate(class_names) } }```### Số hóa biểu đồ đườngĐối với biểu đồ đường, nhiệm vụ số hóa là khôi phục chuỗi dữ liệu $(x, y)$ từ hình ảnh. Quy trình bao gồm phát hiện trục, hiệu chỉnh thang đo và dò đường cong.```{python}#| label: line-chart-digitizer#| eval: falsedef digitize_line_chart(image_path, x_range=None, y_range=None):""" Digitize a line chart image to recover the data series. Parameters ---------- image_path : str Path to chart image. x_range : tuple, optional (x_min, x_max) if known. y_range : tuple, optional (y_min, y_max) if known. Returns ------- DataFrame : Digitized data points (x, y). """import cv2 img = cv2.imread(str(image_path)) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w = gray.shape# Step 1: Detect plot area (largest rectangular region) edges = cv2.Canny(gray, 50, 150) contours, _ = cv2.findContours( edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE )if contours: largest =max(contours, key=cv2.contourArea) x_start, y_start, plot_w, plot_h = cv2.boundingRect(largest)else:# Fallback: assume plot is central 80% of image x_start, y_start =int(w *0.1), int(h *0.1) plot_w, plot_h =int(w *0.8), int(h *0.8)# Step 2: Extract line pixels within plot area# Convert to HSV and isolate colored lines hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) plot_region = hsv[y_start:y_start + plot_h, x_start:x_start + plot_w]# Detect non-white, non-gray pixels (likely the line) saturation = plot_region[:, :, 1] line_mask = saturation >30# Colored pixels# Step 3: Trace the line (column-wise median of colored pixels) data_points = []for col inrange(plot_w): col_pixels = np.where(line_mask[:, col])[0]iflen(col_pixels) >0:# Use median y-position y_pixel = np.median(col_pixels)# Convert pixel to data coordinates x_frac = col / plot_w y_frac =1- y_pixel / plot_h # Invert y-axis x_val = (x_range[0] + x_frac * (x_range[1] - x_range[0])if x_range else x_frac) y_val = (y_range[0] + y_frac * (y_range[1] - y_range[0])if y_range else y_frac) data_points.append({"x": x_val, "y": y_val})return pd.DataFrame(data_points)```## Phân tích cảm xúc thị giác### Cảm xúc của hình ảnh trong tin tức tài chínhCác bài báo tin tức đi kèm các hình ảnh mang cảm xúc độc lập với văn bản. Bức ảnh một CEO mỉm cười tại buổi họp báo truyền tải thông tin khác với bức ảnh cùng CEO đó đối mặt với người biểu tình. @obaid2022picture chứng minh rằng cảm xúc thị giác của các bức ảnh trên Wall Street Journal dự báo lợi nhuận thị trường: những ngày có nhiều hình ảnh tiêu cực hơn đi trước mức lợi nhuận thấp hơn.Chúng ta triển khai phân tích cảm xúc thị giác bằng hai cách tiếp cận: một bộ phân loại cảm xúc tiền huấn luyện và một mô hình thị giác-ngôn ngữ diễn giải hình ảnh trong bối cảnh tài chính.### Cảm xúc thị giác dựa trên CNN```{python}#| label: visual-sentiment#| eval: falsedef compute_visual_sentiment(image_paths, model_name="resnet50"):""" Compute visual sentiment scores using a fine-tuned CNN. Uses features from a pre-trained CNN followed by a sentiment classifier trained on the Visual Sentiment Ontology (VSO) or similar dataset. Parameters ---------- image_paths : list Paths to news images. Returns ------- DataFrame : image_path, positive_score, negative_score, sentiment. """ device ="cuda"if torch.cuda.is_available() else"cpu" model, transform, dim = build_feature_extractor(model_name, device)# Extract features features = extract_features(image_paths, model, transform, device)# Simple sentiment model: use mean activation as proxy# (In practice, fine-tune on labeled financial images)# Higher mean activation in certain feature channels# correlates with positive/negative affect# Positive channels (empirically determined via validation) pos_channels =list(range(0, dim //3)) neg_channels =list(range(dim //3, 2* dim //3)) pos_scores = features[:, pos_channels].mean(axis=1) neg_scores = features[:, neg_channels].mean(axis=1)# Normalize to [0, 1] pos_norm = (pos_scores - pos_scores.min()) / ( pos_scores.max() - pos_scores.min() +1e-8 ) neg_norm = (neg_scores - neg_scores.min()) / ( neg_scores.max() - neg_scores.min() +1e-8 ) sentiment = pos_norm - neg_normreturn pd.DataFrame({"image_path": image_paths,"positive_score": pos_norm,"negative_score": neg_norm,"net_sentiment": sentiment })```### Mô hình thị giác-ngôn ngữ cho việc hiểu hình ảnh tài chínhCách tiếp cận mạnh nhất cho phân tích hình ảnh tài chính sử dụng các mô hình thị giác-ngôn ngữ (VLM), xử lý đồng thời hình ảnh và văn bản. Các mô hình như CLIP [@radford2021learning], BLIP-2 [@li2023blip] và GPT-4V có thể được nhắc lệnh để diễn giải các hình ảnh tài chính trong ngữ cảnh. Chẳng hạn, với một bức ảnh chụp từ trên không của một nhà máy, một VLM có thể trả lời "Nhà máy này có đang hoạt động hết công suất không?" hoặc "Có thấy việc xây dựng thêm các cơ sở không?"```{python}#| label: vlm-analysis#| eval: falsedef vlm_financial_analysis(image_path, prompt, model_name="clip"):""" Use a vision-language model to analyze a financial image. Parameters ---------- image_path : str Path to image. prompt : str Financial analysis prompt. model_name : str 'clip' for zero-shot classification, 'blip2' for visual question answering. Returns ------- dict : Model output (scores or text). """ img = Image.open(image_path).convert("RGB")if model_name =="clip":from transformers import CLIPProcessor, CLIPModel clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32" ) processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32" )# Zero-shot classification with financial labels labels = ["busy commercial area with many customers","empty commercial area with few customers","active construction site with workers","idle construction site without activity","healthy green crops in agricultural field","damaged or dry crops in agricultural field","busy port with many ships and containers","quiet port with few ships" ] inputs = processor( text=labels, images=img, return_tensors="pt", padding=True )with torch.no_grad(): outputs = clip_model(**inputs) logits = outputs.logits_per_image.squeeze() probs = torch.softmax(logits, dim=0) results = { label: prob.item()for label, prob inzip(labels, probs) }return {"scores": results, "top_label": max(results, key=results.get)}elif model_name =="blip2":from transformers import Blip2Processor, Blip2ForConditionalGeneration processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b" ) model = Blip2ForConditionalGeneration.from_pretrained("Salesforce/blip2-opt-2.7b", torch_dtype=torch.float16 ) inputs = processor(images=img, text=prompt, return_tensors="pt")with torch.no_grad(): generated_ids = model.generate(**inputs, max_length=100) answer = processor.decode( generated_ids[0], skip_special_tokens=True )return {"answer": answer}``````{python}#| label: visual-sentiment-market#| eval: false# Construct daily visual sentiment index from news images# Source: Vietnamese financial news sites (VnExpress, CafeF, etc.)news_images = dc.get_news_images( start_date="2018-01-01", end_date="2024-12-31", source=["vnexpress_finance", "cafef"])# Aggregate daily visual sentimentdaily_sentiment = ( news_images.groupby("date") .agg( visual_sentiment=("net_sentiment", "mean"), n_images=("net_sentiment", "count"), pct_negative=("net_sentiment", lambda x: (x <0).mean()) ) .reset_index())# Merge with market returnsmarket_returns = dc.get_market_returns( start_date="2018-01-01", end_date="2024-12-31", frequency="daily")sentiment_returns = daily_sentiment.merge( market_returns[["date", "mkt_ret"]], on="date", how="inner")# Lead-lag analysis: does visual sentiment predict next-day returns?sentiment_returns = sentiment_returns.sort_values("date")sentiment_returns["mkt_ret_lead1"] = sentiment_returns["mkt_ret"].shift(-1)``````{python}#| label: tbl-visual-sentiment-predictability#| eval: false#| tbl-cap: "Cảm xúc thị giác và khả năng dự báo lợi nhuận thị trường"# Regression: next-day return on today's visual sentimentsr_clean = sentiment_returns.dropna( subset=["mkt_ret_lead1", "visual_sentiment", "mkt_ret"])model_sent = sm.OLS( sr_clean["mkt_ret_lead1"], sm.add_constant(sr_clean[["visual_sentiment", "mkt_ret"]])).fit(cov_type="HAC", cov_kwds={"maxlags": 5})sent_results = pd.DataFrame({"Coefficient": model_sent.params.round(6),"Std Error": model_sent.bse.round(6),"t-stat": model_sent.tvalues.round(3),"p-value": model_sent.pvalues.round(4)})sent_results```## Hợp nhất đa phương thức: Kết hợp hình ảnh và văn bản### Vì sao cần đa phương thức?Văn bản và hình ảnh nắm bắt các khía cạnh khác nhau của cùng một thực tế kinh tế nền tảng. Một báo cáo kết quả kinh doanh mô tả hiệu quả tài chính bằng lời và số; các bức ảnh đi kèm cho thấy nhà máy, sản phẩm và ban điều hành. Một bài báo về một cảng mô tả khối lượng thương mại bằng văn bản; ảnh vệ tinh cho thấy vị trí thực tế của các tàu. Kết hợp cả hai phương thức cho ra một biểu diễn phong phú hơn so với từng phương thức riêng lẻ.Kiến trúc hợp nhất phụ thuộc vào ứng dụng:**Hợp nhất sớm.** Nối các đặc trưng hình ảnh $\mathbf{z}^{\text{img}}$ và các đặc trưng văn bản $\mathbf{z}^{\text{txt}}$ thành một vector duy nhất $[\mathbf{z}^{\text{img}}; \mathbf{z}^{\text{txt}}]$ trước khi dự báo. Đơn giản nhưng bỏ qua các tương tác chéo phương thức.**Hợp nhất muộn.** Huấn luyện các mô hình riêng cho từng phương thức và kết hợp các dự báo: $\hat{y} = \alpha \hat{y}^{\text{img}} + (1-\alpha) \hat{y}^{\text{txt}}$. Vững chắc nhưng không thể học các đặc trưng chéo phương thức.**Hợp nhất bằng chú ý chéo.** Dùng cơ chế chú ý chéo (cross-attention) của transformer để mỗi phương thức chú ý đến phương thức còn lại. Mạnh nhất nhưng đòi hỏi nhiều dữ liệu và tính toán hơn.$$\mathbf{z}^{\text{fused}} = \text{CrossAttention}(\mathbf{z}^{\text{img}}, \mathbf{z}^{\text{txt}}) = \text{softmax}\left(\frac{\mathbf{Q}^{\text{img}} (\mathbf{K}^{\text{txt}})^\top}{\sqrt{d}}\right) \mathbf{V}^{\text{txt}}$$ {#eq-cross-attention}```{python}#| label: multimodal-fusion#| eval: falseclass MultimodalFusionModel(nn.Module):""" Multimodal fusion model combining image and text features for financial prediction. Supports early fusion, late fusion, and cross-attention. """def__init__(self, img_dim=2048, txt_dim=768, hidden_dim=256, fusion="early", n_heads=4):super().__init__()self.fusion = fusion# Image projectionself.img_proj = nn.Sequential( nn.Linear(img_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2) )# Text projectionself.txt_proj = nn.Sequential( nn.Linear(txt_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2) )if fusion =="early":self.head = nn.Sequential( nn.Linear(hidden_dim *2, hidden_dim), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden_dim, 1) )elif fusion =="late":self.img_head = nn.Linear(hidden_dim, 1)self.txt_head = nn.Linear(hidden_dim, 1)self.alpha = nn.Parameter(torch.tensor(0.5))elif fusion =="cross_attention":self.cross_attn = nn.MultiheadAttention( embed_dim=hidden_dim, num_heads=n_heads, batch_first=True )self.head = nn.Sequential( nn.Linear(hidden_dim, hidden_dim //2), nn.ReLU(), nn.Linear(hidden_dim //2, 1) )def forward(self, img_features, txt_features): img_h =self.img_proj(img_features) txt_h =self.txt_proj(txt_features)ifself.fusion =="early": combined = torch.cat([img_h, txt_h], dim=-1)returnself.head(combined).squeeze(-1)elifself.fusion =="late": img_pred =self.img_head(img_h).squeeze(-1) txt_pred =self.txt_head(txt_h).squeeze(-1) alpha = torch.sigmoid(self.alpha)return alpha * img_pred + (1- alpha) * txt_predelifself.fusion =="cross_attention":# Image attends to text img_h_unsq = img_h.unsqueeze(1) # (B, 1, D) txt_h_unsq = txt_h.unsqueeze(1) attn_out, _ =self.cross_attn( img_h_unsq, txt_h_unsq, txt_h_unsq )returnself.head(attn_out.squeeze(1)).squeeze(-1)``````{python}#| label: multimodal-experiment#| eval: falsedef run_multimodal_experiment(image_features, text_features, returns, fusion_types=["early", "late","cross_attention"]):""" Compare multimodal fusion strategies for return prediction. Parameters ---------- image_features : np.ndarray Image feature matrix (N x img_dim). text_features : np.ndarray Text feature matrix (N x txt_dim). returns : np.ndarray Target returns (N,). fusion_types : list Fusion strategies to compare. Returns ------- DataFrame : R², MSE, Sharpe for each strategy. """from sklearn.model_selection import TimeSeriesSplit n =len(returns) tscv = TimeSeriesSplit(n_splits=5) results = []for fusion in fusion_types: fold_r2s = []for train_idx, test_idx in tscv.split(returns):# Convert to tensors X_img_train = torch.tensor( image_features[train_idx], dtype=torch.float32 ) X_txt_train = torch.tensor( text_features[train_idx], dtype=torch.float32 ) y_train = torch.tensor( returns[train_idx], dtype=torch.float32 ) X_img_test = torch.tensor( image_features[test_idx], dtype=torch.float32 ) X_txt_test = torch.tensor( text_features[test_idx], dtype=torch.float32 ) y_test = returns[test_idx]# Build and train model model = MultimodalFusionModel( img_dim=image_features.shape[1], txt_dim=text_features.shape[1], fusion=fusion ) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() model.train()for epoch inrange(50): optimizer.zero_grad() pred = model(X_img_train, X_txt_train) loss = loss_fn(pred, y_train) loss.backward() optimizer.step()# Evaluate model.eval()with torch.no_grad(): y_pred = model(X_img_test, X_txt_test).numpy() ss_res = np.sum((y_test - y_pred) **2) ss_tot = np.sum((y_test - y_test.mean()) **2) r2 =1- ss_res / ss_tot if ss_tot >0else0 fold_r2s.append(r2) results.append({"fusion": fusion,"r2_mean": np.mean(fold_r2s),"r2_std": np.std(fold_r2s) })# Add unimodal baselinesfor modality, features in [("image_only", image_features), ("text_only", text_features)]:from sklearn.linear_model import RidgeCV fold_r2s = []for train_idx, test_idx in tscv.split(returns): ridge = RidgeCV(alphas=np.logspace(-3, 3, 10)) ridge.fit(features[train_idx], returns[train_idx]) y_pred = ridge.predict(features[test_idx]) y_test = returns[test_idx] ss_res = np.sum((y_test - y_pred) **2) ss_tot = np.sum((y_test - y_test.mean()) **2) fold_r2s.append(1- ss_res / ss_tot if ss_tot >0else0) results.append({"fusion": modality,"r2_mean": np.mean(fold_r2s),"r2_std": np.std(fold_r2s) })return pd.DataFrame(results)```### Những cân nhắc thực tiễn đối với thị trường Việt NamPhân tích đa phương thức trên thị trường Việt Nam đối mặt với một số cân nhắc thực tiễn:**Căn chỉnh dữ liệu.** Ảnh vệ tinh, bài báo tin tức và dữ liệu thị trường vận hành ở các tần suất thời gian và độ phân giải không gian khác nhau. Ảnh vệ tinh tổng hợp có sẵn hằng tuần hoặc hai tuần một lần; tin tức theo ngày; giao dịch trong ngày. Việc căn chỉnh đúng đòi hỏi xác định tập thông tin mà nhà đầu tư có tại thời điểm ra quyết định giao dịch để tránh thiên lệch nhìn trước.**Khan hiếm nhãn.** Học có giám sát đòi hỏi dữ liệu có gán nhãn (ví dụ: hình ảnh được chú thích bằng các kết quả kinh tế). Ở Việt Nam, các nhãn thực địa (doanh số bán lẻ thực tế, năng suất cây trồng thực tế, thông lượng cảng thực tế) đến với độ trễ đáng kể và thường thiếu độ chi tiết để khớp với độ phân giải của vệ tinh. Do đó, các cách tiếp cận bán giám sát và tự giám sát là thiết yếu.**Cân nhắc về quy định.** Ảnh vệ tinh độ phân giải cao của các cơ sở thương mại hoặc quân sự cụ thể có thể bị hạn chế. Các nhà nghiên cứu nên xác minh rằng các nguồn ảnh của họ tuân thủ quy định của Việt Nam về dữ liệu địa không gian.**Chi phí tính toán.** Xử lý các ô ảnh vệ tinh qua CNN đòi hỏi tính toán nặng. Một ô Sentinel-2 đơn lẻ ở độ phân giải 10m bao phủ Thành phố Hồ Chí Minh chứa khoảng $10{,}980 \times 10{,}980$ điểm ảnh trên mỗi băng tần. Việc chia thành các mảnh ảnh $224 \times 224$ làm đầu vào cho CNN tạo ra $\sim 2{,}400$ mảnh trên mỗi ô, mỗi mảng cần một lượt lan truyền xuôi qua mạng.| Ứng dụng | Nguồn ảnh | Độ phân giải | Tần suất | Khả năng tiếp cận tại Việt Nam ||---------------|---------------|---------------|---------------|---------------|| Độ sáng ban đêm | VIIRS/DMSP | 500m | Hằng tháng | Miễn phí (NOAA/EOG) || Sức khỏe cây trồng | MODIS/Sentinel-2 | 250m/10m | 16 ngày/5 ngày | Miễn phí (NASA/ESA) || Phát hiện cảng/tàu | Sentinel-1 (SAR) | 10m | 12 ngày | Miễn phí (ESA Copernicus) || Giám sát xây dựng | Thương mại (Maxar) | 30cm | Theo yêu cầu | Trả phí (\$) || Mật độ đô thị | Sentinel-2 | 10m | 5 ngày | Miễn phí (ESA) || OCR tài liệu | Hồ sơ của doanh nghiệp | N/A | Theo sự kiện | DataCore.vn || Ảnh tin tức | Truyền thông tài chính | N/A | Hằng ngày | Thu thập dữ liệu web |: Các nguồn dữ liệu hình ảnh cho các ứng dụng tài chính tại Việt Nam {#tbl-image-sources}```{=html}<!-- ## Exercises1. **Nightlight and Firm Performance.** Extend the nightlight analysis by constructing a multi-province exposure measure for firms with geographically diversified operations (e.g., a retailer with stores across multiple provinces). Weight the nightlight signal by the estimated revenue share from each province. Test whether this weighted signal predicts quarterly earnings surprises better than the headquarters-only measure.2. **OCR Accuracy Benchmarking.** Collect a sample of 50 Vietnamese annual reports in scanned PDF format. Manually transcribe key financial figures (revenue, net income, total assets) from 5 reports to create ground truth. Compare OCR accuracy (character error rate, field-level exact match) across PaddleOCR, VietOCR, and Google Cloud Vision. Which engine performs best on Vietnamese financial tables? Does pre-processing (deskewing, contrast enhancement) significantly improve accuracy?3. **Satellite Nowcasting of Industrial Production.** Using Sentinel-2 imagery of industrial zones near Binh Duong, Dong Nai, and Bac Ninh, construct monthly indices of industrial activity based on CNN features. Specifically, train a model to predict the Industrial Production Index (IPI) published by the GSO using satellite features with a one-month lead. Report out-of-sample $R^2$ and compare with a baseline that uses only lagged IPI values (autoregressive benchmark).4. **Visual Sentiment and Earnings Announcements.** For a sample of Vietnamese firms, collect the photographs accompanying earnings-related news coverage on VnExpress and CafeF. Compute visual sentiment scores for each image using a pre-trained model. Test whether visual sentiment around earnings dates (5-day window) predicts post-earnings-announcement drift (PEAD), controlling for the textual sentiment of the same articles. Does the image add information beyond the text?5. **Multimodal Earnings Prediction.** For each firm-quarter observation, construct three feature sets: (a) tabular financial ratios, (b) textual features from the management discussion section of the annual report (using PhoBERT embeddings from the previous chapter), and (c) satellite features of the firm's headquarters region. Implement early, late, and cross-attention fusion and compare forecast accuracy for next-quarter earnings. Which modality contributes most? Does the multimodal model significantly outperform the best unimodal model?6. **Chart Digitization for Historical Vietnamese Market Data.** Obtain scanned charts of the VN-Index from pre-2005 sources (before electronic data was widely available). Digitize the index level time series using the line chart digitization algorithm. Cross-validate against any available electronic data for overlapping periods. Estimate the digitization error and assess whether the recovered time series is sufficiently accurate for computing return statistics (mean, volatility, maximum drawdown). -->```## Tóm tắtChương này đã mở rộng bộ công cụ dữ liệu thay thế từ văn bản (chương trước) sang hình ảnh. Chúng ta đã trình bày năm lĩnh vực ứng dụng riêng biệt của dữ liệu thị giác trên thị trường tài chính Việt Nam.Thứ nhất, ảnh vệ tinh và ảnh địa không gian cung cấp các tín hiệu kinh tế tần suất cao, chi tiết về không gian, đi trước thống kê chính thức. Độ sáng ban đêm đóng vai trò biến đại diện cho GDP cấp tỉnh với $R^2$ theo mặt cắt ngang vượt 0.7; các chỉ số sức khỏe cây trồng NDVI dự báo lợi nhuận của các doanh nghiệp nông nghiệp; và các đặc trưng CNN được trích xuất từ các ô ảnh vệ tinh cho phép các biểu diễn không gian phong phú về hoạt động kinh tế.Thứ hai, phân tích ảnh tài liệu giải quyết vấn đề thực tiễn là trích xuất dữ liệu có cấu trúc từ các hồ sơ tài chính của Việt Nam vốn đến dưới dạng ảnh quét. Quy trình (ví dụ: OCR với các công cụ được tối ưu cho tiếng Việt, phân tích bố cục, trích xuất bảng và hiểu tài liệu dựa trên LayoutLM) chuyển các điểm ảnh phi cấu trúc thành dữ liệu tài chính có cấu trúc mà mọi phân tích phía sau đều cần.Thứ ba, số hóa biểu đồ khôi phục các chuỗi dữ liệu số từ các biểu diễn thị giác, mở rộng phạm vi lịch sử và cho phép sử dụng có hệ thống các sản phẩm của nhà phân tích. Thứ tư, phân tích cảm xúc thị giác từ hình ảnh tin tức cung cấp một chiều tín hiệu trực giao với cảm xúc văn bản, với sức dự báo tiềm năng đối với lợi nhuận thị trường.Thứ năm, hợp nhất đa phương thức (kết hợp các biểu diễn hình ảnh và văn bản thông qua các kiến trúc hợp nhất sớm, muộn hoặc chú ý chéo) cho ra các mô hình dự báo phong phú hơn so với từng phương thức riêng lẻ. Lợi ích thực tiễn của các cách tiếp cận đa phương thức tăng theo mức độ đa dạng và chất lượng của dữ liệu sẵn có, khiến nó ngày càng phù hợp khi hệ sinh thái dữ liệu thay thế của Việt Nam trưởng thành.Mạch xuyên suốt tất cả các ứng dụng là quy trình biến đổi: tensor điểm ảnh thô $\to$ biểu diễn đặc trưng (qua CNN, ViT hoặc VLM) $\to$ tín hiệu tài chính $\to$ diễn giải kinh tế. Việc lựa chọn kiến trúc và chất lượng của việc thích ứng miền quyết định liệu tín hiệu thu được có nội dung dự báo thực sự hay chỉ nắm bắt nhiễu.